././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1674658642.017574 repoze.who-3.0.0/0000775000175000017500000000000000000000000014761 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/.bzrignore0000664000175000017500000000004300000000000016760 0ustar00tseavertseaver00000000000000.coverage *.egg-info docs/.build/* ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643734808.0 repoze.who-3.0.0/.gitignore0000664000175000017500000000014300000000000016747 0ustar00tseavertseaver00000000000000build/ dist/ docs/.build/ docs/_build/ .tox/ *.pyc *.egg-info .coverage coverage.xml nosetests.xml ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1674658590.0 repoze.who-3.0.0/CHANGES.rst0000664000175000017500000006207200000000000016572 0ustar00tseavertseaver00000000000000repoze.who Changelog ==================== 3.0.0 (2023-01-16) ------------------ - No changes from 3.0.0b1. 3.0.0b1 (2023-01-16) -------------------- - Add support for Python 3.9, 3.10 and 3.11. - Drop support for Python 2.7, 3.4, 3.5, and 3.6. - Add Github Actions workflow to exercise unit tests / coverage. 2.4.1 (2022-02-01) ------------------ - Disallow separators in AuthTicket component values. Closes #37. - Handle bytes / string correctly in 'repoze.who.plugins.htpasswd.sha1_check'. Closes #28. - Switch to use ``pytest`` as the testrunner. Closes #34. 2.4 (2020-06-03) ---------------- - Add support for Python 3.6, 3.7, and 3.8. - Drop support for Python 3.3. - Fix travis configuration. - Add ``samesite`` option to AuthTktCookiePlugin constructor. If this is passed, it should be a string, and it will be used to compose the Set-Cookie header's "SameSite" value, e.g. if you pass ``samesite="Strict"`` into the constructor, the cookie value for the auth tkt cooke will contain ``SameSite=Strict``. 2.3 (2016-05-31) ---------------- - Add support for Python 3.4, Python 3.5, and PyPy3. - Drop support for Python 2.6 and 3.2. - ``middleware``: avoid passing extracted ``identity`` to ``remember`` during egress (the app may have called ``api.forget()``). See #21. - ``_auth_tkt`` / ``plugins.auth_tkt``: add support for any hash algorithm supported by the ``hashlib`` module in Python's standard library. Fixes #22 via #23. - ``plugins.auth_tkt``: Fix storage of "userdata" to save dict. Fixes #14 via #18. - middleware: avoid UnboundLocalError when wrapped generater yields no items. See: http://bugs.repoze.org/issue184 - Make cookie expiration date RFC-2616 compliant (independent of locale, including 'GMT' zone). See #11. 2.2 (2013-05-17) ---------------- - Parse INI-file configuration using ``SafeConfigParser``: allows escaping the ``'%'`` so that e.g. a query template using for a DB-API connection using ``pyformat`` preserves the template. - Added support for Python 3.3, PyPy. 2.1 (2013-03-20) ---------------- - ``_compat`` module: tolerate missing ``CONTENT_TYPE`` key in the WSGI environment. Thanks to Dag Hoidal for the patch. - ``htpasswd`` plugin: add a ``sha1_check`` checker function (the ``crypt`` module is not available on Windows). Thanks to Chandrashekar Jayaraman for the patch. - Documentation typo fixes from Carlos de la Guardia and Atsushi Odagiri. 2.1b1 (2012-11-05) ------------------ - Ported to Py3k using the "compatible subset" mode. - Dropped support for Python < 2.6.x. - Dropped dependency on Paste (forking some code from it). - Added dependency on WebOb instead. Thanks to Atsushi Odagiri (aodag) for the initial effort. 2.0 (2011-09-28) ---------------- - ``auth_tkt`` plugin: strip any port number from the 'Domain' of generated cookies. http://bugs.repoze.org/issue66 - Further harden middleware, calling ``close()`` on the iterable even if raising an exception for a missing challenger. http://bugs.repoze.org/issue174 2.0b1 (2011-05-24) ------------------ - Enabled standard use of logging module's configuration mechanism. See http://docs.python.org/dev/howto/logging.html#configuring-logging-for-a-library Thanks to jgoldsmith for the patch: http://bugs.repoze.org/issue178 - ``repoze.who.plugins.htpasswd``: defend against timing-based attacks. 2.0a4 (2011-02-02) ------------------ - Ensure that the middleware calls ``close()`` (if it exists) on the iterable returned from thw wrapped application, as required by PEP 333. http://bugs.repoze.org/issue174 - Make ``make_api_factory_with_config`` tolerant of invalid filenames / content for the config file: in such cases, the API factory will have *no* configured plugins or policies: it will only be useful for retrieving the API from an environment populated by middleware. - Fix bug in ``repoze.who.api`` where the ``remember()`` or ``forget()`` methods could return a None if the identifier plugin returned a None. - Fix ``auth_tkt`` plugin to not hand over tokens as strings to paste. See http://lists.repoze.org/pipermail/repoze-dev/2010-November/003680.html - Fix ``auth_tkt`` plugin to add "secure" and "HttpOnly" to cookies when configured with ``secure=True``: these attributes prevent the browser from sending cookies over insecure channels, which could be vulnerable to some XSS attacks. - Avoid propagating unicode 'max_age' value into cookie headers. See https://bugs.launchpad.net/bugs/674123 . - Added a single-file example BFG application demonstrating the use of the new 'login' and 'logout' methods of the API object. - Add ``login`` and ``logout`` methods to the ``repoze.who.api.API`` object, as a convenience for application-driven login / logout code, which would otherwise need to use private methods of the API, and reach down into its plugins. 2.0a3 (2010-09-30) ------------------ - Deprecated the following plugins, moving their modules, tests, and docs to a new project, ``repoze.who.deprecatedplugins``: - ``repoze.who.plugins.cookie.InsecureCookiePlugin`` - ``repoze.who.plugins.form.FormPlugin`` - ``repoze.who.plugins.form.RedirectingFormPlugin`` - Made the ``repoze.who.plugins.cookie.InsecureCookiePlugin`` take a ``charset`` argument, and use to to encode / decode login and password. See http://bugs.repoze.org/issue155 - Updated ``repoze.who.restrict`` to return headers as a list, to keep ``wsgiref`` from complaining. - Helped default request classifier cope with xml submissions with an explicit charset defined: http://bugs.repoze.org/issue145 (Lorenzo M. Catucci) - Corrected the handling of type and subtype when matching an XML post to ``xmlpost`` in the default classifier, which, according to RFC 2045, must be matched case-insensitively: http://bugs.repoze.org/issue145 (Lorenzo M. Catucci) - Added ``repoze.who.config:make_api_factory_with_config``, a convenience method for applications which want to set up their own API Factory from a configuration file. - Fixed example call to ``repoze.who.config:make_middleware_with_config`` (added missing ``global_config`` argument). See http://bugs.repoze.org/issue114 2.0a2 (2010-03-25) ------------------ Bugs Fixed ~~~~~~~~~~ - Fixed failure to pass substution values in log message string formatting for ``repoze.who.api:API.challenge``. Fix included adding tests for all logging done by the API object. See http://bugs.repoze.org/issue122 Backward Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~ - Adjusted logging level for some lower-level details from ``info`` to ``debug``. 2.0a1 (2010-02-24) ------------------ Features ~~~~~~~~ - Restored the ability to create the middleware using the old ``classifier`` argument. That argument is now a deprecated-but-will-work-forever alias for ``request_classifier``. - The ``auth_tkt`` plugin now implements the ``IAuthenticator`` interface, and should normally be used both as an ``IIdentifier`` and an ``IAuthenticator``. - Factored out the API of the middleware object to make it useful from within the application. Applications using ``repoze.who``` now fall into one of three catgeories: - "middleware-only" applications are configured with middleware, and use either ``REMOTE_USER`` or ``repoze.who.identity`` from the environment to determing the authenticated user. - "bare metal" applications use no ``repoze.who`` middleware at all: instead, they configure and an ``APIFactory`` object at startup, and use it to create an ``API`` object when needed on a per-request basis. - "hybrid" applications are configured with ``repoze.who`` middleware, but use a new library function to fetch the ``API`` object from the environ, e.g. to permit calling ``remember`` after a signup or successful login. Bugs Fixed ~~~~~~~~~~ - Fix http://bugs.repoze.org/issue102: when no challengers existed, logging would cause an exception. - Remove ``ez_setup.py`` and dependency on it in setup.py (support distribute). Backward Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~ - The middleware used to allow identifier plugins to "pre-authenticate" an identity. This feature is no longer supported: the ``auth_tkt`` plugin, which used to use the feature, is now configured to work as an authenticator plugin (as well as an identifier). - The ``repoze.who.middleware:PluggableAuthenticationMiddleware`` class no longer has the following (non-API) methods (now made API methods of the ``repoze.who.api:API`` class): - ``add_metadata`` - ``authenticate`` - ``challenge`` - ``identify`` - The following (non-API) functions moved from ``repoze.who.middleware`` to ``repoze.who.api``: - ``make_registries`` - ``match_classification`` - ``verify`` 1.0.18 (2009-11-05) ------------------- - Issue #104: AuthTkt plugin was passing an invalid cookie value in headers from ``forget``, and was not setting the ``Max-Age`` and ``Expires`` attributes of those cookies. 1.0.17 (2009-11-05) ------------------- - Fixed the ``repoze.who.plugins.form.make_plugin`` factory's ``formcallable`` argument handling, to allow passing in a dotted name (e.g., from a config file). 1.0.16 (2009-11-04) ------------------- - Exposed ``formcallable`` argument for ``repoze.who.plugins.form.FormPlugin`` to the callers of the ``repoze.who.plugins.form.make_plugin`` factory. Thanks to Roland Hedburg for the report. - Fixed an issue that caused the following symptom when using the ini configuration parser:: TypeError: _makePlugin() got multiple values for keyword argument 'name' See http://bugs.repoze.org/issue92 for more details. Thanks to vaab for the bug report and initial fix. 1.0.15 (2009-06-25) ------------------- - If the form post value ``max_age`` exists while in the ``identify`` method is handling the ``login_handler_path``, pass the max_age value in the returned identity dictionary as ``max_age``. See the below bullet point for why. - If the ``identity`` dict passed to the ``auth_tkt`` ``remember`` method contains a ``max_age`` key with a string (or integer) value, treat it as a cue to set the ``Max-Age`` and ``Expires`` headers in the returned cookies. The cookie ``Max-Age`` is set to the value and the ``Expires`` is computed from the current time. 1.0.14 (2009-06-17) ------------------- - Fix test breakage on Windows. See http://bugs.repoze.org/issue79 . - Documented issue with using ``include_ip`` setting in the ``auth_tkt`` plugin. See http://bugs.repoze.org/issue81 . - Added 'passthrough_challenge_decider', which avoids re-challenging 401 responses which have been "pre-challenged" by the application. - One-hundred percent unit test coverage. - Add ``timeout`` and ``reissue_time`` arguments to the auth_tkt identifier plugin, courtesty of Paul Johnston. - Add a ``userid_checker`` argument to the auth_tkt identifier plugin, courtesty of Gustavo Narea. If ``userid_checker`` is provided, it must be a dotted Python name that resolves to a function which accepts a userid and returns a boolean True or False, indicating whether that user exists in a database. This is a workaround. Due to a design bug in repoze.who, the only way who can check for user existence is to use one or more IAuthenticator plugin ``authenticate`` methods. If an IAuthenticator's ``authenticate`` method returns true, it means that the user exists. However most IAuthenticator plugins expect *both* a username and a password, and will return False unconditionally if both aren't supplied. This means that an authenticator can't be used to check if the user "only" exists. The identity provided by an auth_tkt does not contain a password to check against. The actual design bug in repoze.who is this: when a user presents credentials from an auth_tkt, he is considered "preauthenticated". IAuthenticator.authenticate is just never called for a "preauthenticated" identity, which works fine, but it means that the user will be considered authenticated even if you deleted the user's record from whatever database you happen to be using. However, if you use a userid_checker, you can ensure that a user exists for the auth_tkt supplied userid. If the userid_checker returns False, the auth_tkt credentials are considered "no good". 1.0.13 (2009-04-24) ------------------- - Added a paragraph to ``IAuthenticator`` docstring, documenting that plugins are allowed to add keys to the ``identity`` dictionary (e.g., to save a second database query in an ``IMetadataProvider`` plugin). - Patch supplied for issue #71 (http://bugs.repoze.org/issue71) whereby a downstream app can return a generator, relying on an upstream component to call start_response. We do this because the challenge decider needs the status and headers to decide what to do. 1.0.12 (2009-04-19) ------------------- - auth_tkt plugin tried to append REMOTE_USER_TOKENS data to existing tokens data returned by auth_tkt.parse_tkt; this was incorrect; just overwrite. - Extended auth_tkt plugin factory to allow passing secret in a separate file from the main config file. See http://bugs.repoze.org/issue40 . 1.0.11 (2009-04-10) ------------------- - Fix auth_tkt plugin; cookie values are now quoted, making it possible to put spaces and other whitespace, etc in usernames. (thanks to Michael Pedersen). - Fix corner case issue of an exception raised when attempting to log when there are no identifiers or authenticators. 1.0.10 (2009-01-23) ------------------- - The RedirectingFormPlugin now passes along SetCookie headers set into the response by the application within the NotFound response (fixes TG2 "flash" issue). 1.0.9 (2008-12-18) ------------------ - The RedirectingFormPlugin now attempts to find a header named ``X-Authentication-Failure-Reason`` among the response headers set by the application when a challenge is issued. If a value for this header exists (and is non-blank), the value is attached to the redirect URL's query string as the ``reason`` parameter (or a user-settable key). This makes it possible for downstream applications to issue a response that initiates a challenge with this header and subsequently display the reason in the login form rendered as a result of the challenge. 1.0.8 (2008-12-13) ------------------ - The ``PluggableAuthenticationMiddleware`` constructor accepts a ``log_stream`` argument, which is typically a file. After this release, it can also be a PEP 333 ``Logger`` instance; if it is a PEP 333 ``Logger`` instance, this logger will be used as the repoze.who logger (instead of one being constructed by the middleware, as was previously always the case). When the ``log_stream`` argument is a PEP 333 Logger object, the ``log_level`` argument is ignored. 1.0.7 (2008-08-28) ------------------ - ``repoze.who`` and ``repoze.who.plugins`` were not added to the ``namespace_packages`` list in setup.py, potentially making 1.0.6 a brownbag release, given that making these packages namespace packages was the only reason for its release. 1.0.6 (2008-08-28) ------------------ - Make repoze.who and repoze.who.plugins into namespace packages mainly so we can allow plugin authors to distribute packages in the repoze.who.plugins namespace. 1.0.5 (2008-08-23) ------------------ - Fix auth_tkt plugin to set the same cookies in its ``remember`` method that it does in its ``forget`` method. Previously, logging out and relogging back in to a site that used auth_tkt identifier plugin was slightly dicey and would only work sometimes. - The FormPlugin plugin has grown a redirect-on-unauthorized feature. Any response from a downstream application that causes a challenge and includes a Location header will cause a redirect to the value of the Location header. 1.0.4 (2008-08-22) ------------------ - Added a key to the '[general]' config section: ``remote_user_key``. If you use this key in the config file, it tells who to 1) not perform any authentication if it exists in the environment during ingress and 2) to set the key in the environment for the downstream app to use as the REMOTE_USER variable. The default is ``REMOTE_USER``. - Using unicode user ids in combination with the auth_tkt plugin would cause problems under mod_wsgi. - Allowed 'cookie_path' argument to InsecureCookiePlugin (and config constructor). Thanks to Gustavo Narea. 1.0.3 (2008-08-16) ------------------ - A bug in the middleware's ``authenticate`` method made it impossible to authenticate a user with a userid that was null (e.g. 0, False), which are valid identifiers. The only invalid userid is now None. - Applied patch from Olaf Conradi which logs an error when an invalid filename is passed to the HTPasswdPlugin. 1.0.2 (2008-06-16) ------------------ - Fix bug found by Chris Perkins: the auth_tkt plugin's "remember" method didn't handle userids which are Python "long" instances properly. Symptom: TypeError: cannot concatenate 'str' and 'long' objects in "paste.auth.auth_tkt". - Added predicate-based "restriction" middleware support (repoze.who.restrict), allowing configuratio-driven authorization as a WSGI filter. One example predicate, 'authenticated_predicate', is supplied, which requires that the user be authenticated either via 'REMOTE_USER' or via 'repoze.who.identity'. To use the filter to restrict access:: [filter:authenticated_only] use = egg:repoze.who#authenticated or:: [filter:some_predicate] use = egg:repoze.who#predicate predicate = my.module:some_predicate some_option = a value 1.0.1 (2008-05-24) ------------------ - Remove dependency-link to dist.repoze.org to prevent easy_install from inserting that path into its search paths (the dependencies are available from PyPI). 1.0 (2008-05-04) ----------------- - The plugin at plugins.form.FormPlugin didn't redirect properly after collecting identification information. Symptom: a downstream app would receive a POST request with a blank body, which would sometimes result in a Bad Request error. - Fixed interface declarations of 'classifiers.default_request_classifier' and 'classifiers.default_password_compare'. - Added actual config-driven middleware factory, 'config.make_middleware_with_config' - Removed fossilized 'who_conf' argument from plugin factory functions. - Added ConfigParser-based WhoConfig, implementing the spec outlined at http://www.plope.com/static/misc/sphinxtest/intro.html#middleware-configuration-via-config-file, with the following changes: - "Bare" plugins (requiring no configuration options) may be specified as either egg entry points (e.g., 'egg:distname#entry_point_name') or as dotted-path-with-colon (e.g., 'dotted.name:object_id'). - Therefore, the separator between a plugin and its classifier is now a semicolon, rather than a colon. E.g.:: [plugins:id_plugin] use = egg:another.package#identify_with_frobnatz frobnatz = baz [identifiers] plugins = egg:my.egg#identify;browser dotted.name:identifier id_plugin 0.9.1 (2008-04-27) ------------------ - Fix auth_tkt plugin to be able to encode and decode integer user ids. 0.9 (2008-04-01) ---------------- - Fix bug introduced in FormPlugin in 0.8 release (rememberer headers not set). - Add PATH_INFO to started and ended log info. - Add a SQLMetadataProviderPlugin (in plugins/sql). - Change constructor of SQLAuthenticatorPlugin: it now accepts only "query", "conn_factory", and "compare_fn". The old constructor accepted a DSN, but some database systems don't use DBAPI DSNs. The new constructor accepts no DSN; the conn_factory is assumed to do all the work to make a connection, including knowing the DSN if one is required. The "conn_factory" should return something that, when called with no arguments, returns a database connection. - The "make_plugin" helper in plugins/sql has been renamed "make_authenticator_plugin". When called, this helper will return a SQLAuthenticatorPlugin. A bit of helper logic in the "make_authenticator_plugin" allows a connection factory to be computed. The top-level callable referred to by conn_factory in this helper should return a function that, when called with no arguments, returns a datbase connection. The top-level callable itself is called with "who_conf" (global who configuration) and any number of non-top-level keyword arguments as they are passed into the helper, to allow for a DSN or URL or whatever to be passed in. - A "make_metatata_plugin" helper has been added to plugins/sql. When called, this will make a SQLMetadataProviderPlugin. See the implementation for details. It is similar to the "make_authenticator_plugin" helper. 0.8 (2008-03-27) ---------------- - Add a RedirectingFormIdentifier plugin. This plugin is willing to redirect to an external (or downstream application) login form to perform identification. The external login form must post to the "login_handler_path" of the plugin (optimally with a "came_from" value to tell the plugin where to redirect the response to if the authentication works properly). The "logout_handler_path" of this plugin can be visited to perform a logout. The "came_from" value also works there. - Identifier plugins are now permitted to set a key in the environment named 'repoze.who.application' on ingress (in 'identify'). If an identifier plugin does so, this application is used instead of the "normal" downstream application. This feature was added to more simply support the redirecting form identifier plugin. 0.7 (2008-03-26) ---------------- - Change the IMetadataProvider interface: this interface used to have a "metadata" method which returned a dictionary. This method is not part of that API anymore. It's been replaced with an "add_metadata" method which has the signature:: def add_metadata(environ, identity): """ Add metadata to the identity (which is a dictionary) """ The return value is ignored. IMetadataProvider plugins are now assumed to be responsible for 'scribbling' directly on the identity that is passed in (it's a dictionary). The user id can always be retrieved from the identity via identity['repoze.who.userid'] for metadata plugins that rely on that value. 0.6 (2008-03-20) ---------------- - Renaming: repoze.pam is now repoze.who - Bump ez_setup.py version. - Add IMetadataProvider plugin type. Chris says 'Whit rules'. 0.5 (2008-03-09) ---------------- - Allow "remote user key" (default: REMOTE_USER) to be overridden (pass in remote_user_key to middleware constructor). - Allow form plugin to override the default form. - API change: IIdentifiers are no longer required to put both 'login' and 'password' in a returned identity dictionary. Instead, an IIdentifier can place arbitrary key/value pairs in the identity dictionary (or return an empty dictionary). - API return value change: the "failure" identity which IIdentifiers return is now None rather than an empty dictionary. - The IAuthenticator interface now specifies that IAuthenticators must not raise an exception when evaluating an identity that does not have "expected" key/value pairs (e.g. when an IAuthenticator that expects login and password inspects an identity returned by an IP-based auth system which only puts the IP address in the identity); instead they fail gracefully by returning None. - Add (cookie) "auth_tkt" identification plugin. - Stamp identity dictionaries with a userid by placing a key named 'repoze.pam.userid' into the identity for each authenticated identity. - If an IIdentifier plugin inserts a 'repoze.pam.userid' key into the identity dictionary, consider this identity "preauthenticated". No authenticator plugins will be asked to authenticate this identity. This is designed for things like the recently added auth_tkt plugin, which embeds the user id into the ticket. This effectively alllows an IIdentifier plugin to become an IAuthenticator plugin when breaking apart the responsibility into two separate plugins is "make-work". Preauthenticated identities will be selected first when deciding which identity to use for any given request. - Insert a 'repoze.pam.identity' key into the WSGI environment on ingress if an identity is found. Its value will be the identity dictionary related to the identity selected by repoze.pam on ingress. Downstream consumers are allowed to mutate this dictionary; this value is passed to "remember" and "forget", so its main use is to do a "credentials reset"; e.g. a user has changed his username or password within the application, but we don't want to force him to log in again after he does so. 0.4 (03-07-2008) ---------------- - Allow plugins to specify a classifiers list per interface (instead of a single classifiers list per plugin). 0.3 (03-05-2008) ---------------- - Make SQLAuthenticatorPlugin's default_password_compare use hexdigest sha instead of base64'ed binary sha for simpler conversion. 0.2 (03-04-2008) ---------------- - Added SQLAuthenticatorPlugin (see plugins/sql.py). 0.1 (02-27-2008) ---------------- - Initial release (no configuration file support yet). ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/CONTRIBUTORS.txt0000664000175000017500000001111200000000000017453 0ustar00tseavertseaver00000000000000Repoze Project Contributor Agreement ==================================== The submitter agrees by adding his or her name within the section below named "Contributors" and submitting the resulting modified document to the canonical shared repository location for this software project (whether directly, as a user with "direct commit access", or via a "pull request"), he or she is signing a contract electronically. The submitter becomes a Contributor after a) he or she signs this document by adding their name beneath the "Contributors" section below, and b) the resulting document is accepted into the canonical version control repository. Treatment of Account --------------------- Contributor will not allow anyone other than the Contributor to use his or her username or source repository login to submit code to a Repoze Project source repository. Should Contributor become aware of any such use, Contributor will immediately by notifying Agendaless Consulting. Notification must be performed by sending an email to webmaster@agendaless.com. Until such notice is received, Contributor will be presumed to have taken all actions made through Contributor's account. If the Contributor has direct commit access, Agendaless Consulting will have complete control and discretion over capabilities assigned to Contributor's account, and may disable Contributor's account for any reason at any time. Legal Effect of Contribution ---------------------------- Upon submitting a change or new work to a Repoze Project source Repository (a "Contribution"), you agree to assign, and hereby do assign, a one-half interest of all right, title and interest in and to copyright and other intellectual property rights with respect to your new and original portions of the Contribution to Agendaless Consulting. You and Agendaless Consulting each agree that the other shall be free to exercise any and all exclusive rights in and to the Contribution, without accounting to one another, including without limitation, the right to license the Contribution to others under the Repoze Public License. This agreement shall run with title to the Contribution. Agendaless Consulting does not convey to you any right, title or interest in or to the Program or such portions of the Contribution that were taken from the Program. Your transmission of a submission to the Repoze Project source Repository and marks of identification concerning the Contribution itself constitute your intent to contribute and your assignment of the work in accordance with the provisions of this Agreement. License Terms ------------- Code committed to the Repoze Project source repository (Committed Code) must be governed by the Repoze Public License (http://repoze.org/LICENSE.txt, aka "the RPL") or another license acceptable to Agendaless Consulting. Until Agendaless Consulting declares in writing an acceptable license other than the RPL, only the RPL shall be used. A list of exceptions is detailed within the "Licensing Exceptions" section of this document, if one exists. Representations, Warranty, and Indemnification ---------------------------------------------- Contributor represents and warrants that the Committed Code does not violate the rights of any person or entity, and that the Contributor has legal authority to enter into this Agreement and legal authority over Contributed Code. Further, Contributor indemnifies Agendaless Consulting against violations. Cryptography ------------ Contributor understands that cryptographic code may be subject to government regulations with which Agendaless Consulting and/or entities using Committed Code must comply. Any code which contains any of the items listed below must not be checked-in until Agendaless Consulting staff has been notified and has approved such contribution in writing. - Cryptographic capabilities or features - Calls to cryptographic features - User interface elements which provide context relating to cryptography - Code which may, under casual inspection, appear to be cryptographic. Notices ------- Contributor confirms that any notices required will be included in any Committed Code. Licensing Exceptions ==================== None. List of Contributors ==================== The below-signed are contributors to a code repository that is part of the project named "repoze.who". Each below-signed contributor has read, understand and agrees to the terms above in the section within this document entitled "Repoze Project Contributor Agreement" as of the date beside his or her name. Contributors ------------ - Tres Seaver, 2011/02/22 - Atsushi Odagiri, 2012/03/22 - Chandrashekar Jayaraman, 2012/11/09 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/COPYRIGHT.txt0000664000175000017500000000015500000000000017073 0ustar00tseavertseaver00000000000000Copyright (c) 2007 Agendaless Consulting and Contributors. (http://www.agendaless.com), All Rights Reserved ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/LICENSE.txt0000664000175000017500000000337700000000000016616 0ustar00tseavertseaver00000000000000License A copyright notice accompanies this license document that identifies the copyright holders. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1674658642.017574 repoze.who-3.0.0/PKG-INFO0000664000175000017500000010425200000000000016062 0ustar00tseavertseaver00000000000000Metadata-Version: 2.1 Name: repoze.who Version: 3.0.0 Summary: repoze.who is an identification and authentication framework for WSGI. Home-page: http://www.repoze.org Author: Agendaless Consulting Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: repoze.who ========== .. image:: https://travis-ci.org/repoze/repoze.who.png?branch=master :target: https://travis-ci.org/repoze/repoze.who .. image:: https://readthedocs.org/projects/repozewho/badge/?version=latest :target: http://repozewho.readthedocs.org/en/latest/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/repoze.who.svg :target: https://pypi.python.org/pypi/repoze.who .. image:: https://img.shields.io/pypi/pyversions/repoze.who.svg :target: https://pypi.python.org/pypi/repoze.who ``repoze.who`` is an identification and authentication framework for arbitrary WSGI applications. ``repoze.who`` can be configured either as WSGI middleware or as an API for use by an application. ``repoze.who`` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but ``repoze.who`` is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application. Installation ------------ Install using setuptools, e.g. (within a virtualenv):: $ easy_install repoze.who or using pip:: $ pip install repoze.who Usage ----- For details on using the various components, please see the documentation in ``docs/index.rst``. A rendered version of that documentation is also available online: - http://repozewho.readthedocs.org/en/latest/ Reporting Bugs -------------- Please report bugs in this package to https://github.com/repoze/repoze.who/issues Obtaining Source Code --------------------- Download development or tagged versions of the software by visiting: https://github.com/repoze/repoze.who repoze.who Changelog ==================== 3.0.0 (2023-01-16) ------------------ - No changes from 3.0.0b1. 3.0.0b1 (2023-01-16) -------------------- - Add support for Python 3.9, 3.10 and 3.11. - Drop support for Python 2.7, 3.4, 3.5, and 3.6. - Add Github Actions workflow to exercise unit tests / coverage. 2.4.1 (2022-02-01) ------------------ - Disallow separators in AuthTicket component values. Closes #37. - Handle bytes / string correctly in 'repoze.who.plugins.htpasswd.sha1_check'. Closes #28. - Switch to use ``pytest`` as the testrunner. Closes #34. 2.4 (2020-06-03) ---------------- - Add support for Python 3.6, 3.7, and 3.8. - Drop support for Python 3.3. - Fix travis configuration. - Add ``samesite`` option to AuthTktCookiePlugin constructor. If this is passed, it should be a string, and it will be used to compose the Set-Cookie header's "SameSite" value, e.g. if you pass ``samesite="Strict"`` into the constructor, the cookie value for the auth tkt cooke will contain ``SameSite=Strict``. 2.3 (2016-05-31) ---------------- - Add support for Python 3.4, Python 3.5, and PyPy3. - Drop support for Python 2.6 and 3.2. - ``middleware``: avoid passing extracted ``identity`` to ``remember`` during egress (the app may have called ``api.forget()``). See #21. - ``_auth_tkt`` / ``plugins.auth_tkt``: add support for any hash algorithm supported by the ``hashlib`` module in Python's standard library. Fixes #22 via #23. - ``plugins.auth_tkt``: Fix storage of "userdata" to save dict. Fixes #14 via #18. - middleware: avoid UnboundLocalError when wrapped generater yields no items. See: http://bugs.repoze.org/issue184 - Make cookie expiration date RFC-2616 compliant (independent of locale, including 'GMT' zone). See #11. 2.2 (2013-05-17) ---------------- - Parse INI-file configuration using ``SafeConfigParser``: allows escaping the ``'%'`` so that e.g. a query template using for a DB-API connection using ``pyformat`` preserves the template. - Added support for Python 3.3, PyPy. 2.1 (2013-03-20) ---------------- - ``_compat`` module: tolerate missing ``CONTENT_TYPE`` key in the WSGI environment. Thanks to Dag Hoidal for the patch. - ``htpasswd`` plugin: add a ``sha1_check`` checker function (the ``crypt`` module is not available on Windows). Thanks to Chandrashekar Jayaraman for the patch. - Documentation typo fixes from Carlos de la Guardia and Atsushi Odagiri. 2.1b1 (2012-11-05) ------------------ - Ported to Py3k using the "compatible subset" mode. - Dropped support for Python < 2.6.x. - Dropped dependency on Paste (forking some code from it). - Added dependency on WebOb instead. Thanks to Atsushi Odagiri (aodag) for the initial effort. 2.0 (2011-09-28) ---------------- - ``auth_tkt`` plugin: strip any port number from the 'Domain' of generated cookies. http://bugs.repoze.org/issue66 - Further harden middleware, calling ``close()`` on the iterable even if raising an exception for a missing challenger. http://bugs.repoze.org/issue174 2.0b1 (2011-05-24) ------------------ - Enabled standard use of logging module's configuration mechanism. See http://docs.python.org/dev/howto/logging.html#configuring-logging-for-a-library Thanks to jgoldsmith for the patch: http://bugs.repoze.org/issue178 - ``repoze.who.plugins.htpasswd``: defend against timing-based attacks. 2.0a4 (2011-02-02) ------------------ - Ensure that the middleware calls ``close()`` (if it exists) on the iterable returned from thw wrapped application, as required by PEP 333. http://bugs.repoze.org/issue174 - Make ``make_api_factory_with_config`` tolerant of invalid filenames / content for the config file: in such cases, the API factory will have *no* configured plugins or policies: it will only be useful for retrieving the API from an environment populated by middleware. - Fix bug in ``repoze.who.api`` where the ``remember()`` or ``forget()`` methods could return a None if the identifier plugin returned a None. - Fix ``auth_tkt`` plugin to not hand over tokens as strings to paste. See http://lists.repoze.org/pipermail/repoze-dev/2010-November/003680.html - Fix ``auth_tkt`` plugin to add "secure" and "HttpOnly" to cookies when configured with ``secure=True``: these attributes prevent the browser from sending cookies over insecure channels, which could be vulnerable to some XSS attacks. - Avoid propagating unicode 'max_age' value into cookie headers. See https://bugs.launchpad.net/bugs/674123 . - Added a single-file example BFG application demonstrating the use of the new 'login' and 'logout' methods of the API object. - Add ``login`` and ``logout`` methods to the ``repoze.who.api.API`` object, as a convenience for application-driven login / logout code, which would otherwise need to use private methods of the API, and reach down into its plugins. 2.0a3 (2010-09-30) ------------------ - Deprecated the following plugins, moving their modules, tests, and docs to a new project, ``repoze.who.deprecatedplugins``: - ``repoze.who.plugins.cookie.InsecureCookiePlugin`` - ``repoze.who.plugins.form.FormPlugin`` - ``repoze.who.plugins.form.RedirectingFormPlugin`` - Made the ``repoze.who.plugins.cookie.InsecureCookiePlugin`` take a ``charset`` argument, and use to to encode / decode login and password. See http://bugs.repoze.org/issue155 - Updated ``repoze.who.restrict`` to return headers as a list, to keep ``wsgiref`` from complaining. - Helped default request classifier cope with xml submissions with an explicit charset defined: http://bugs.repoze.org/issue145 (Lorenzo M. Catucci) - Corrected the handling of type and subtype when matching an XML post to ``xmlpost`` in the default classifier, which, according to RFC 2045, must be matched case-insensitively: http://bugs.repoze.org/issue145 (Lorenzo M. Catucci) - Added ``repoze.who.config:make_api_factory_with_config``, a convenience method for applications which want to set up their own API Factory from a configuration file. - Fixed example call to ``repoze.who.config:make_middleware_with_config`` (added missing ``global_config`` argument). See http://bugs.repoze.org/issue114 2.0a2 (2010-03-25) ------------------ Bugs Fixed ~~~~~~~~~~ - Fixed failure to pass substution values in log message string formatting for ``repoze.who.api:API.challenge``. Fix included adding tests for all logging done by the API object. See http://bugs.repoze.org/issue122 Backward Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~ - Adjusted logging level for some lower-level details from ``info`` to ``debug``. 2.0a1 (2010-02-24) ------------------ Features ~~~~~~~~ - Restored the ability to create the middleware using the old ``classifier`` argument. That argument is now a deprecated-but-will-work-forever alias for ``request_classifier``. - The ``auth_tkt`` plugin now implements the ``IAuthenticator`` interface, and should normally be used both as an ``IIdentifier`` and an ``IAuthenticator``. - Factored out the API of the middleware object to make it useful from within the application. Applications using ``repoze.who``` now fall into one of three catgeories: - "middleware-only" applications are configured with middleware, and use either ``REMOTE_USER`` or ``repoze.who.identity`` from the environment to determing the authenticated user. - "bare metal" applications use no ``repoze.who`` middleware at all: instead, they configure and an ``APIFactory`` object at startup, and use it to create an ``API`` object when needed on a per-request basis. - "hybrid" applications are configured with ``repoze.who`` middleware, but use a new library function to fetch the ``API`` object from the environ, e.g. to permit calling ``remember`` after a signup or successful login. Bugs Fixed ~~~~~~~~~~ - Fix http://bugs.repoze.org/issue102: when no challengers existed, logging would cause an exception. - Remove ``ez_setup.py`` and dependency on it in setup.py (support distribute). Backward Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~ - The middleware used to allow identifier plugins to "pre-authenticate" an identity. This feature is no longer supported: the ``auth_tkt`` plugin, which used to use the feature, is now configured to work as an authenticator plugin (as well as an identifier). - The ``repoze.who.middleware:PluggableAuthenticationMiddleware`` class no longer has the following (non-API) methods (now made API methods of the ``repoze.who.api:API`` class): - ``add_metadata`` - ``authenticate`` - ``challenge`` - ``identify`` - The following (non-API) functions moved from ``repoze.who.middleware`` to ``repoze.who.api``: - ``make_registries`` - ``match_classification`` - ``verify`` 1.0.18 (2009-11-05) ------------------- - Issue #104: AuthTkt plugin was passing an invalid cookie value in headers from ``forget``, and was not setting the ``Max-Age`` and ``Expires`` attributes of those cookies. 1.0.17 (2009-11-05) ------------------- - Fixed the ``repoze.who.plugins.form.make_plugin`` factory's ``formcallable`` argument handling, to allow passing in a dotted name (e.g., from a config file). 1.0.16 (2009-11-04) ------------------- - Exposed ``formcallable`` argument for ``repoze.who.plugins.form.FormPlugin`` to the callers of the ``repoze.who.plugins.form.make_plugin`` factory. Thanks to Roland Hedburg for the report. - Fixed an issue that caused the following symptom when using the ini configuration parser:: TypeError: _makePlugin() got multiple values for keyword argument 'name' See http://bugs.repoze.org/issue92 for more details. Thanks to vaab for the bug report and initial fix. 1.0.15 (2009-06-25) ------------------- - If the form post value ``max_age`` exists while in the ``identify`` method is handling the ``login_handler_path``, pass the max_age value in the returned identity dictionary as ``max_age``. See the below bullet point for why. - If the ``identity`` dict passed to the ``auth_tkt`` ``remember`` method contains a ``max_age`` key with a string (or integer) value, treat it as a cue to set the ``Max-Age`` and ``Expires`` headers in the returned cookies. The cookie ``Max-Age`` is set to the value and the ``Expires`` is computed from the current time. 1.0.14 (2009-06-17) ------------------- - Fix test breakage on Windows. See http://bugs.repoze.org/issue79 . - Documented issue with using ``include_ip`` setting in the ``auth_tkt`` plugin. See http://bugs.repoze.org/issue81 . - Added 'passthrough_challenge_decider', which avoids re-challenging 401 responses which have been "pre-challenged" by the application. - One-hundred percent unit test coverage. - Add ``timeout`` and ``reissue_time`` arguments to the auth_tkt identifier plugin, courtesty of Paul Johnston. - Add a ``userid_checker`` argument to the auth_tkt identifier plugin, courtesty of Gustavo Narea. If ``userid_checker`` is provided, it must be a dotted Python name that resolves to a function which accepts a userid and returns a boolean True or False, indicating whether that user exists in a database. This is a workaround. Due to a design bug in repoze.who, the only way who can check for user existence is to use one or more IAuthenticator plugin ``authenticate`` methods. If an IAuthenticator's ``authenticate`` method returns true, it means that the user exists. However most IAuthenticator plugins expect *both* a username and a password, and will return False unconditionally if both aren't supplied. This means that an authenticator can't be used to check if the user "only" exists. The identity provided by an auth_tkt does not contain a password to check against. The actual design bug in repoze.who is this: when a user presents credentials from an auth_tkt, he is considered "preauthenticated". IAuthenticator.authenticate is just never called for a "preauthenticated" identity, which works fine, but it means that the user will be considered authenticated even if you deleted the user's record from whatever database you happen to be using. However, if you use a userid_checker, you can ensure that a user exists for the auth_tkt supplied userid. If the userid_checker returns False, the auth_tkt credentials are considered "no good". 1.0.13 (2009-04-24) ------------------- - Added a paragraph to ``IAuthenticator`` docstring, documenting that plugins are allowed to add keys to the ``identity`` dictionary (e.g., to save a second database query in an ``IMetadataProvider`` plugin). - Patch supplied for issue #71 (http://bugs.repoze.org/issue71) whereby a downstream app can return a generator, relying on an upstream component to call start_response. We do this because the challenge decider needs the status and headers to decide what to do. 1.0.12 (2009-04-19) ------------------- - auth_tkt plugin tried to append REMOTE_USER_TOKENS data to existing tokens data returned by auth_tkt.parse_tkt; this was incorrect; just overwrite. - Extended auth_tkt plugin factory to allow passing secret in a separate file from the main config file. See http://bugs.repoze.org/issue40 . 1.0.11 (2009-04-10) ------------------- - Fix auth_tkt plugin; cookie values are now quoted, making it possible to put spaces and other whitespace, etc in usernames. (thanks to Michael Pedersen). - Fix corner case issue of an exception raised when attempting to log when there are no identifiers or authenticators. 1.0.10 (2009-01-23) ------------------- - The RedirectingFormPlugin now passes along SetCookie headers set into the response by the application within the NotFound response (fixes TG2 "flash" issue). 1.0.9 (2008-12-18) ------------------ - The RedirectingFormPlugin now attempts to find a header named ``X-Authentication-Failure-Reason`` among the response headers set by the application when a challenge is issued. If a value for this header exists (and is non-blank), the value is attached to the redirect URL's query string as the ``reason`` parameter (or a user-settable key). This makes it possible for downstream applications to issue a response that initiates a challenge with this header and subsequently display the reason in the login form rendered as a result of the challenge. 1.0.8 (2008-12-13) ------------------ - The ``PluggableAuthenticationMiddleware`` constructor accepts a ``log_stream`` argument, which is typically a file. After this release, it can also be a PEP 333 ``Logger`` instance; if it is a PEP 333 ``Logger`` instance, this logger will be used as the repoze.who logger (instead of one being constructed by the middleware, as was previously always the case). When the ``log_stream`` argument is a PEP 333 Logger object, the ``log_level`` argument is ignored. 1.0.7 (2008-08-28) ------------------ - ``repoze.who`` and ``repoze.who.plugins`` were not added to the ``namespace_packages`` list in setup.py, potentially making 1.0.6 a brownbag release, given that making these packages namespace packages was the only reason for its release. 1.0.6 (2008-08-28) ------------------ - Make repoze.who and repoze.who.plugins into namespace packages mainly so we can allow plugin authors to distribute packages in the repoze.who.plugins namespace. 1.0.5 (2008-08-23) ------------------ - Fix auth_tkt plugin to set the same cookies in its ``remember`` method that it does in its ``forget`` method. Previously, logging out and relogging back in to a site that used auth_tkt identifier plugin was slightly dicey and would only work sometimes. - The FormPlugin plugin has grown a redirect-on-unauthorized feature. Any response from a downstream application that causes a challenge and includes a Location header will cause a redirect to the value of the Location header. 1.0.4 (2008-08-22) ------------------ - Added a key to the '[general]' config section: ``remote_user_key``. If you use this key in the config file, it tells who to 1) not perform any authentication if it exists in the environment during ingress and 2) to set the key in the environment for the downstream app to use as the REMOTE_USER variable. The default is ``REMOTE_USER``. - Using unicode user ids in combination with the auth_tkt plugin would cause problems under mod_wsgi. - Allowed 'cookie_path' argument to InsecureCookiePlugin (and config constructor). Thanks to Gustavo Narea. 1.0.3 (2008-08-16) ------------------ - A bug in the middleware's ``authenticate`` method made it impossible to authenticate a user with a userid that was null (e.g. 0, False), which are valid identifiers. The only invalid userid is now None. - Applied patch from Olaf Conradi which logs an error when an invalid filename is passed to the HTPasswdPlugin. 1.0.2 (2008-06-16) ------------------ - Fix bug found by Chris Perkins: the auth_tkt plugin's "remember" method didn't handle userids which are Python "long" instances properly. Symptom: TypeError: cannot concatenate 'str' and 'long' objects in "paste.auth.auth_tkt". - Added predicate-based "restriction" middleware support (repoze.who.restrict), allowing configuratio-driven authorization as a WSGI filter. One example predicate, 'authenticated_predicate', is supplied, which requires that the user be authenticated either via 'REMOTE_USER' or via 'repoze.who.identity'. To use the filter to restrict access:: [filter:authenticated_only] use = egg:repoze.who#authenticated or:: [filter:some_predicate] use = egg:repoze.who#predicate predicate = my.module:some_predicate some_option = a value 1.0.1 (2008-05-24) ------------------ - Remove dependency-link to dist.repoze.org to prevent easy_install from inserting that path into its search paths (the dependencies are available from PyPI). 1.0 (2008-05-04) ----------------- - The plugin at plugins.form.FormPlugin didn't redirect properly after collecting identification information. Symptom: a downstream app would receive a POST request with a blank body, which would sometimes result in a Bad Request error. - Fixed interface declarations of 'classifiers.default_request_classifier' and 'classifiers.default_password_compare'. - Added actual config-driven middleware factory, 'config.make_middleware_with_config' - Removed fossilized 'who_conf' argument from plugin factory functions. - Added ConfigParser-based WhoConfig, implementing the spec outlined at http://www.plope.com/static/misc/sphinxtest/intro.html#middleware-configuration-via-config-file, with the following changes: - "Bare" plugins (requiring no configuration options) may be specified as either egg entry points (e.g., 'egg:distname#entry_point_name') or as dotted-path-with-colon (e.g., 'dotted.name:object_id'). - Therefore, the separator between a plugin and its classifier is now a semicolon, rather than a colon. E.g.:: [plugins:id_plugin] use = egg:another.package#identify_with_frobnatz frobnatz = baz [identifiers] plugins = egg:my.egg#identify;browser dotted.name:identifier id_plugin 0.9.1 (2008-04-27) ------------------ - Fix auth_tkt plugin to be able to encode and decode integer user ids. 0.9 (2008-04-01) ---------------- - Fix bug introduced in FormPlugin in 0.8 release (rememberer headers not set). - Add PATH_INFO to started and ended log info. - Add a SQLMetadataProviderPlugin (in plugins/sql). - Change constructor of SQLAuthenticatorPlugin: it now accepts only "query", "conn_factory", and "compare_fn". The old constructor accepted a DSN, but some database systems don't use DBAPI DSNs. The new constructor accepts no DSN; the conn_factory is assumed to do all the work to make a connection, including knowing the DSN if one is required. The "conn_factory" should return something that, when called with no arguments, returns a database connection. - The "make_plugin" helper in plugins/sql has been renamed "make_authenticator_plugin". When called, this helper will return a SQLAuthenticatorPlugin. A bit of helper logic in the "make_authenticator_plugin" allows a connection factory to be computed. The top-level callable referred to by conn_factory in this helper should return a function that, when called with no arguments, returns a datbase connection. The top-level callable itself is called with "who_conf" (global who configuration) and any number of non-top-level keyword arguments as they are passed into the helper, to allow for a DSN or URL or whatever to be passed in. - A "make_metatata_plugin" helper has been added to plugins/sql. When called, this will make a SQLMetadataProviderPlugin. See the implementation for details. It is similar to the "make_authenticator_plugin" helper. 0.8 (2008-03-27) ---------------- - Add a RedirectingFormIdentifier plugin. This plugin is willing to redirect to an external (or downstream application) login form to perform identification. The external login form must post to the "login_handler_path" of the plugin (optimally with a "came_from" value to tell the plugin where to redirect the response to if the authentication works properly). The "logout_handler_path" of this plugin can be visited to perform a logout. The "came_from" value also works there. - Identifier plugins are now permitted to set a key in the environment named 'repoze.who.application' on ingress (in 'identify'). If an identifier plugin does so, this application is used instead of the "normal" downstream application. This feature was added to more simply support the redirecting form identifier plugin. 0.7 (2008-03-26) ---------------- - Change the IMetadataProvider interface: this interface used to have a "metadata" method which returned a dictionary. This method is not part of that API anymore. It's been replaced with an "add_metadata" method which has the signature:: def add_metadata(environ, identity): """ Add metadata to the identity (which is a dictionary) """ The return value is ignored. IMetadataProvider plugins are now assumed to be responsible for 'scribbling' directly on the identity that is passed in (it's a dictionary). The user id can always be retrieved from the identity via identity['repoze.who.userid'] for metadata plugins that rely on that value. 0.6 (2008-03-20) ---------------- - Renaming: repoze.pam is now repoze.who - Bump ez_setup.py version. - Add IMetadataProvider plugin type. Chris says 'Whit rules'. 0.5 (2008-03-09) ---------------- - Allow "remote user key" (default: REMOTE_USER) to be overridden (pass in remote_user_key to middleware constructor). - Allow form plugin to override the default form. - API change: IIdentifiers are no longer required to put both 'login' and 'password' in a returned identity dictionary. Instead, an IIdentifier can place arbitrary key/value pairs in the identity dictionary (or return an empty dictionary). - API return value change: the "failure" identity which IIdentifiers return is now None rather than an empty dictionary. - The IAuthenticator interface now specifies that IAuthenticators must not raise an exception when evaluating an identity that does not have "expected" key/value pairs (e.g. when an IAuthenticator that expects login and password inspects an identity returned by an IP-based auth system which only puts the IP address in the identity); instead they fail gracefully by returning None. - Add (cookie) "auth_tkt" identification plugin. - Stamp identity dictionaries with a userid by placing a key named 'repoze.pam.userid' into the identity for each authenticated identity. - If an IIdentifier plugin inserts a 'repoze.pam.userid' key into the identity dictionary, consider this identity "preauthenticated". No authenticator plugins will be asked to authenticate this identity. This is designed for things like the recently added auth_tkt plugin, which embeds the user id into the ticket. This effectively alllows an IIdentifier plugin to become an IAuthenticator plugin when breaking apart the responsibility into two separate plugins is "make-work". Preauthenticated identities will be selected first when deciding which identity to use for any given request. - Insert a 'repoze.pam.identity' key into the WSGI environment on ingress if an identity is found. Its value will be the identity dictionary related to the identity selected by repoze.pam on ingress. Downstream consumers are allowed to mutate this dictionary; this value is passed to "remember" and "forget", so its main use is to do a "credentials reset"; e.g. a user has changed his username or password within the application, but we don't want to force him to log in again after he does so. 0.4 (03-07-2008) ---------------- - Allow plugins to specify a classifiers list per interface (instead of a single classifiers list per plugin). 0.3 (03-05-2008) ---------------- - Make SQLAuthenticatorPlugin's default_password_compare use hexdigest sha instead of base64'ed binary sha for simpler conversion. 0.2 (03-04-2008) ---------------- - Added SQLAuthenticatorPlugin (see plugins/sql.py). 0.1 (02-27-2008) ---------------- - Initial release (no configuration file support yet). Keywords: web application server wsgi zope Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Provides-Extra: docs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1464979179.0 repoze.who-3.0.0/README.rst0000664000175000017500000000336700000000000016461 0ustar00tseavertseaver00000000000000repoze.who ========== .. image:: https://travis-ci.org/repoze/repoze.who.png?branch=master :target: https://travis-ci.org/repoze/repoze.who .. image:: https://readthedocs.org/projects/repozewho/badge/?version=latest :target: http://repozewho.readthedocs.org/en/latest/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/repoze.who.svg :target: https://pypi.python.org/pypi/repoze.who .. image:: https://img.shields.io/pypi/pyversions/repoze.who.svg :target: https://pypi.python.org/pypi/repoze.who ``repoze.who`` is an identification and authentication framework for arbitrary WSGI applications. ``repoze.who`` can be configured either as WSGI middleware or as an API for use by an application. ``repoze.who`` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but ``repoze.who`` is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application. Installation ------------ Install using setuptools, e.g. (within a virtualenv):: $ easy_install repoze.who or using pip:: $ pip install repoze.who Usage ----- For details on using the various components, please see the documentation in ``docs/index.rst``. A rendered version of that documentation is also available online: - http://repozewho.readthedocs.org/en/latest/ Reporting Bugs -------------- Please report bugs in this package to https://github.com/repoze/repoze.who/issues Obtaining Source Code --------------------- Download development or tagged versions of the software by visiting: https://github.com/repoze/repoze.who ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/TODO.txt0000664000175000017500000000125300000000000016270 0ustar00tseavertseaver00000000000000repoze.who TODOs ================ Features -------- - [X} Config file parser. - [_] Allow the ZODB plugin to connect read-only. - [X] Add a ``RedirectorPlugin``, similar to ``plugins.form.RedirectingFormPlugin``, but which *only* implements the ``IChallenger`` interface. - [_] Deprecate the form plugins. Documentation ------------- - [_] Middleware use case as tutorial: bug tracker + ``REMOTE_USER`` - [_] Middleware use case as tutorial: wiki + ``repoze.who.identity`` - [_] API use case as tutorial: wiki w/ login and logout views - [_] API use case as tutorial: multiple apps w/ SSO - [_] Hybrid use case as tutorial: multiple apps w/ SSO, one OTS ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/docs/0000775000175000017500000000000000000000000015711 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/docs/.static/0000775000175000017500000000000000000000000017256 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/.static/ingress.png0000664000175000017500000026202600000000000021446 0ustar00tseavertseaver00000000000000‰PNG  IHDR6 0N(sBIT|dˆ pHYs † †æ²4tEXtSoftwarewww.inkscape.org›î< IDATxœìÝy`åÝðïœ{'„ÜÜ !!HTôÕª=´µ­µZVÛzÕ{x¶Ö+++±hÑ"¬[·ãÇÇìÙ³¡ª*–/_Žõë×£°°#FŒÀe—]†¼¼¼f_ƒ×ë5dîØ±^¯ùùù6l.¼ðB8ΨëmÙ²%%%-~¯ žžuÙþýû±qãFlݺÇŽÃàÁƒ‘ŸŸéÓ§·ºÛaË–-8vìXÔe6› S¦L1=·xñbãâ & 11ÑX¶iÓ&œ8q"b;Ó¦Mƒ,ËÖAQ¬Y³ë֭áC‡0`ÀÌœ9¹¹¹ÍÖ¿¢¢›6mÂÎ;±sçN8ãó9r$D±m¾f4MÖ-[ŒÏ½´´yyyÈÏÏÇ…^ˆ”””&×?yò$.\ˆ]»v᪫®ÂW\سg¾þúklÙ²)))7n\Ä{ÞZ^¯K–,ÁŠ+гgO<øàƒQ_Ï×_E‹¡ºº¯¼ò DQ„ÇãÁW_}… 6àĉBII }ôQc/½ôöîÝ‹ñãÇcÚ´i¿_ªª¿ [¶l¢(ÈÏÏÇÈ‘#1a„&÷Y^^Ž÷Þ{À‘#GžžŽ¡C‡"//C‡m´Þ7nÄŠ+pèÐ!TTT`àÀÆzƒ jògtŒ±JFâÞâÅ‹Ù~ðÖ½{w€`.—‹1ÆXEE»ôÒKç°ªª*ÓúË—/g}ûö5–sgºÿË_þ’ÕÕÕåkjjØ?þñ6uêT&I’Qöî»ïfK—.eC† 1íãyž=ýôÓM¾Ž+V°þýû›Ö ­KFF[¸paÔu§M›±Ï¦nÿýï#¶QSSÃ~úÓŸ6ºNÿþýÙêÕ«[õÙ|ï{ßkt{½zõ2•ݳgiùܹsM˯½öÚ¨Û)--eŒ1öóŸÿÜôüÎ;Ù+¯¼Âúõ뱎Ãá`Ÿ|òI£õÖ4ýãÿ`‰‰‰Æ:‚ ˜¶QPPÀ¶nÝÚª÷#š;w²Q£F™¶º/§ÓÉ^~ùe¦iši½ÒÒRöüóϳ‹/¾ØôsòÄO°eË–±‰'F}¿îºë®³ªçÇÌnºé&ær¹Œm]pÁ¦2«W¯f?ûÙÏXFF†iŸ§OŸfO?ý4KKK‹¨OZZÛ¸qc£ûýðÃY^^ž©|èû“““æL™bºÍ™3‡mܸ‘ÝsÏ=¬OŸ>¦ýñĦ2W^y¥iyqq1[½z5s: {î¹çØ®]»Œ~øëŒös¬|>_D}>›4i’Q.11‘-\¸¹ÝnvàÀvË-·˜ÞË¿ýío-~?½öÚkL–ec{wß}7;zô(«ªªbo½õ³Z­Æ²K/½ÔTßwÞy'êë ¾?MÝZPcÌn·Gl'<Ø >ü¬ê4zôè¨ûüÏþc*÷Æo0ÆÛµkWÄ,¡·ï}ï{ì’K.‰ºlÿþýlêÔ©QŸZ°`ñÞ§¥¥±O>ù„¹Ýn¶wï^vÑEëdee1¯×kªóßþö7c¹$IlîܹìÔ©SlÇŽìÅ_d‡ƒ0ý‘âóùLÛ5j[¶l+//gŸþ¹ñB÷îÝ[ý¹‘øBÁ¦‹¨¬¬d¦/†„„„ˆ¿½Õ!hõêÕF˜°Ûí¬¸¸Ø´ÝÐ/(léÒ¥Œ1ÆEalûöí¦åcÆŒaŸ|ò «¨¨`ÕÕÕlÉ’%,;;ÛTF–eVXXhÚÏâÅ‹Mežxâ‰ˆ×øè£šÊ|ðÁ¦åÁ`óä“Ošž¯­­e999¦uÿð‡?Dl?ô@~ÿý÷G,ýK{̘1Í|"‘ž}öYS¢µ– 6ÌTÆf³™ZÊcìØ±c Ð[\<iYx°y衇ؒ%K؉'Xyy9{ë­·˜Íf3•ùôÓO#êñûßÿÞTfÙ²e¦åªªšŒV«•û¬±LÓ4Ö­[7cÙÍ7ßñ>N˜0!"ؼ÷Þ{¦m®\¹Ò´Î¡C‡(ØÆ›.gܸq¦/‡Ûn»­[·ŽUTT°gžy†`cÇŽeŒé!…¶æDkÙ±c‡i{“'O6-?räˆiùÝwß±²²2–ššÚèA¥ººšõêÕËt¬¨¨ˆØNII‰©Û«gÏž¬¦¦ÆX 6óçÏ7­þ¥~ÑE1EQLeÞ~ûmS™£GFìÿšk®1•Y»vm´ Q»ví2­çwš–ïß¿Ÿ‘­,áÍõÁ°yíµ×Fì#<Ø:t(¢LxwÖ«¯¾jZþí·ßšÞçáÇG}=K—.5mgÊ”)­z?4MccÆŒ1m#Z·–ªª¬wïÞM†¨O>ùÄ´ðpËXd@ ÿYn¦‚MÐøñãMû ¡Œ16räHS™Ï?ÿÜ´üÃ?lò÷KUU–’’b,1bDÔºüò—¿4mgêÔ©lÑ¢E¬¦¦†-X°€Ùl6æp8Œò¡?#Ûóûý¦€Ú=´{÷nÓ¾Î;(.ÄÛo¿="ØÜ{ヲõ^zé%Ó:@€‰¢HÁ†0:+ª ãysçÎÅèѣѭ[7<øàƒ¨ªªÂÒ¥K6lÀž={ŒòÑöæåå!))Éx¼{÷îV×£{÷îxàLÏ­_¿Þ¸¿|ùr?~ÜxN§ÿ÷ÿ‡’’œôë×O=õ”iýû÷7ZÏÆÌ˜1øèÐ!÷¦œÍA¿=5÷Y\~ùåEŠ¢Ð[7C­[·>ŸÏx|ýõ×ç:Eûü£Šþ;Êž~úiœ9s‹/6ž+..ÆwÜ—^z úÓŸ0uêTÓú.— ï¿ÿ>n¿ývœ:uÊx~ÅŠ1bn½õV<ùä“èÙ³çwyy$P°!ª®®6=~üñÇM- m)ü€ât:‘šš @o!i©ð9Sjkk£–{ýõ×MÍØøç?ÿµlh‹dgg›º¸ÚÒøñãát:zƒM°µföìÙ€©S§B–eøý~ú_ÍcÆŒùÎÝP-ÑŸGGî'–åææâw¿ûüqú\=>ø zè!œ9s÷ß¿QöꫯÆ=÷Üó÷þûpóÍ7ãá‡nÕ6222°hÑ"Ì;÷ß?ªªªŒe»víÂW\믿 ,0}7L›6 ;wîÄwÞ‰wß}×x^Ó4¼ùæ›xï½÷0þ|S×$éz:ן'¤S ï;?pà@»íëðáæÇC† Çq€üü|Ó²¦Æ>„þ%Æ ‹(SXXˆ»ï¾ÛôÜ믿ÞèoÙÙÙ°Z­Æã³ébj)Y–Mª\¹r%|>Ÿ1¾&l\..»ì2£ÜG}UU¿€¯¾úêv«c[m±ŸðeáëÆ³Ç{ o¿ý¶Ñ…:gÎtëÖ ¹¹¹Øºu+úõ뇹sçâƒ>0~§¾‹ðq<ßå{áöÛoÇ‘#Gð‡?üÝ»w7-{ï½÷ð÷¿ÿ=bîÝ»cÁ‚زe fÍšezMn··Ür ÊËËϺN$öQ°! 6áƒYÛRø ÈÐSøÁðèÑ£n'tq´uUUÅ­·Þjú‹þÎ;ï4f¡ô±=ö˜Ñ‚Ãó<l,¯®®Æºuëš{IQ©ªÚìØÐV±ºº:¼óÎ;ؼy3Î?ÿ|ÓøŸÐð²wï^¼ùæ›(++È#¢nn+­ù<ÂgAnM° /ÛØìÌ@äç~Þyçµx?ñà–[nÁ§Ÿ~ «ÕŠ1cÆ`îܹøâ‹/PTT„ÂÂBÜvÛmm6tø Þ`øn©-[¶à…^0º´ºu놇~EEE¸õÖ[MeC»ÖÞzë-¼øâ‹ÆïÏù矅 bË–-¦1VuuuذaC«_‰lH£ÆŒc'òùçŸãóÏ?oršššV}Éc Ï=÷œñØb±˜Î’š0a‚1pÐûÔûÉý -´‹()))b<Çœ9s°fÍãñ AƒðÇ?þÑTfåÊ•xüñÇM]S¡ƒuDœÅMð, yóæ¡ÿþ0`æÍ›×èzÓ§O7ýœŽ?ØZtÕUW™Êýæ7¿оÝP@dkЇ~µÜºuëLgA]pÁƒ‰›2dÈÓ=………ضm[Ô²‹-2î ‚€+¯¼²Åû‰3fÌ€×ëÅ”)SpÛm·aÒ¤IèÓ§O›´Ò„êÝ»·écÇŽáÅ_lrEQŒî¦åË—ãW¿úUÄ™kN§sçÎ5ã ý½ê©§pÏ=÷ggàÉ'Ÿ4=ºéz(ØF¥¤¤˜ \ÿõ¯E”­©©ÁÓO?¬¬¬ˆzsüqÓ`χ~4§§§ãÙgŸ5{<Ì;7b;o¿ý¶éŒ¬^xÁ§[·nÅc=f<–$ óçχÍf3m§°°0bÛ¿ûÝïпãñš5k0}útœ9s&¢ìÚµk1yòd<ôÐCÆs¥¥¥¸óÎ;¿ ï¼óÎF¯[•––† /¼Ðx áÁ¦G¦3˜‚åÚ;Ø\vÙe¸å–[ŒÇ[¶l‰Ú‚õꫯ÷eYÆë¯¿Þªýˆ¢h:€i`uÐÊ•+M§øÞÿýQO g+V¬0Π{÷Ýwqï½÷âÕW_=ëAÔÍùç?ÿiûòÛßþøÃ"ÎÚRo¼ñˆÕ«W›–Eû=«¬¬4ýatþùçG”‰¶^èï¡ M^»ŽtŒ&èë2E1]£‰ã8vòäÉ&×Ñ4]qÅ3öîݛ͘1ƒÍž=› :Ô4åýñãÇõÃ'è»êª«ØÚµkÙîÝ»Ù|ÀfÏžmZþýïŸùýþfë!›7os»ÝÌív³wÞyÇ4iÜ7Þ±ð™eo½õVöÍ7ߘn[·n5f÷ ŸXmÅŠ³òZ­VvÁ°›o¾™M˜0Á4ûðüccÝO?ý4â=\¼xq£ï{ø¥¢M‚ÆcO=õ”©\NNN“Ÿç¬Y³Lå£]6à³Ï>3•¹ãŽ;"Ê”””˜fŒNIIa_~ù%SU••••±‡zÈXÆó<{á…š¬WSî¹çS}yäVQQÁûâ‹/L×?5jTÄôýŒ16oÞ¼&'?dLŸ…7))É(Ó«W¯¨ÛjNYY™é÷¡ÿþ“ï)ŠÂ†jªÓáÇ#¶õâ‹/šÊÌ™3'ê>,Xñó¼¥§§³›nº‰Í›7;v,êú7Üpƒi%K–4û:C?ãПƒ‰'²›nº‰9Ò¸4Ð0“öœ9s _×mþüùF>lº6ÕàÁƒMl0€`³fÍbË—/guuuLQ¶jÕ*–••e¬÷øã7[wß(ØtS§N5}Ù†tÙ}÷Ý×躪ª²—_~¹ÉkÙˆ¢È®¿þz¶fÍÓºáÁ¦±[vvvĬªá‚] =øX­VÓëÊÌÌdï¾ûnÔõ£]ǧ©[´c|8&Nœqúº(ŠX³f ^y刢ˆŸýìgMæ>|8žzê)£¾¡]?¡òòòðì³ÏÂãñÐ'ï‹fìØ±e9ê²ÐIÐn¿ýöFO³íô‰ÖæÌ™ƒ;î¸k׮ŶmÛ°wï^ôèÑÃø<.¹ä’fçbi‰éÓ§cüøñXµj¶mÛ†íÛ·CUUc?_|1ÒÓÓ#ÖëÑ£GÄp¡‚Ÿñ¥—^uŽ&Q· ÏóøùÏÞèòàû<}útäææF-œG–eÜu×]n+8öèÛo¿Å]wÝ…¯¿þÇá?ÿù¦Nн{÷bûöíX²d >ÿüscLšÇãÁ 7Ü€òòrH’„K.¹Ä4é]¸hóÓ„ºòÊ+1~üx|ñÅÆçâ÷ûï…K.¹$â$„;ï¼ãÇÇÞ½{±oß>ìÝ»‡ÆÈ‘#‘““ƒI“&E#µbÅ ìÙ³{÷î5n555èß¿?rssñƒü b>,Ò5qŒ±J‰Í–$ä,™&êºûî»ñ /t\…‰#×^{­qyƒ‰'bÙ²ee¼^/¦M›†/¿üÒxnýúõMþ¡CH,£Áă***Lgƒ…Ï/dµZqÓM7yž7 „'$ÞP°!„¼”lÚ´©Ñr¡óD;¶ÑÉ( ‰l!$%%%™®(¿~ýz<òÈ#§\øá‡Æ©÷ýû÷7®1FH¼¢16¤]TWWcùòåXµj•iò®üü|üö·¿Ejj*&NœØ5$$öcìØ±¦«À8ãLJ$IØ´i6lØŽãpÅWàïÿ{»ÎJMHg@Á†´‹ 6`ôèÑ.ONNnõD~„Ho¿ý6æÍ›‡ÂÂBc¢Æ´´4dggcذa¸ë®»".…@H¼¢`CÚ…×ëÅ‘#G].Š"rrrÎ]…é"¼^/TU…ÃáèèªÒ!(ØB!$nÐàaB!„Ä 6„B‰l!„7(ØB!$nP°!„BHÜ `C!„¸AÁ†B!qƒ‚ !„BâB!„Ä 6„B‰l!„7(ØB!$nP°!„BHÜ `C!„¸AÁ†B!qƒ‚ !„BâB!„Ä 6„B‰l!„7(ØB!$nP°!„BHÜ `C!„¸AÁ†B!qƒ‚ !„BâB!„Ä 6„B‰l!„7ÄŽ®@G`ŒutâÇq]B!]T\›ðÃÔJhJ1 6çÇqàx+x©¨¡BH{‰Ë` 4ZàÕ ðnêÙÍ_ØÁ5#ï„`-€h;’ã2H®]%B!q„cŒUHì芴…Ð_Å\xŠÓj:°F¤9’s=æ—zutU!„ĸ 6z¨aPý'à>ý(µŸ›–sœ^L€ vÇISÉ.Ži^¨J%T¥¡Ýœ{ÆK°t»¹ã*G!$.ÄEWT°¥FUªQ{t4ÿac/8aO8‚˜€µvŒ°±NLCÀ{ÞºÝ`,¦V¡îÄ÷æ‡%éGTGB!ñ æ[lŒñ4š÷©Ÿ Põ¦±Ìê‹=.Ð0V¨ðYýÿC«_V–5Üg¡ë×o×´>B»çÂ÷ö\èYdùˆ}²hë…o¯¡<@í%x^hªžÚmPügô2¼‰ý¿/gB91lBC¿f<'®1–ÙFB²d†”­ÿ÷œÖ0Vl˜ù~}Wkâ9€]n-ÙBÿ=‹uYì?4Xו$’B”wõf|§¢ãR$ô[ŽNF !„Ä„˜=ï–…85M·øc™(¥¡†1@«ot9Æw™‹v;»û´Lþj”Ù wÞRcuÇË¥nüUïFlBi‰˜ 6@}hÑ4<{Á”£õÏr°'ŽÐ—#,ÌttÊh£˜ð‹@Q{óë`àQW[%àÏ˰:òeÚÏÚøBé*b2Ø»7ôÖ Š{³±L”“Áqú˜h¦k ]Â9ÓIRKªŒèᆺjjв5§½Q<[ÏM !„Ę 6€jcP5ª¯á@(JiõËØ4m­‚PH Nð Žw€ã,%à…ªú‚¨õRý{Á´º¶Ù?!„.%&ƒM0ÔhšUQ¡ùw˹{°Ô9©‹ª2óÉHm¤©¡³…aÇÞ:ìØS =_§žL(p¸ IDATJ¸h¡§µc{ƒá†À .Ó¢€_1‚Ø­¾¬ Õ·ë»TœBHsóØ0ÆÀq\C7”¢€©uõÇYž³¶{êÜ*>\|ÛvV£¸Ô@¯VLž†KǤ´Ù~¼> -9€áЉÉp9õZ[«âO=xîÑ~HKîˆñlZw8ðB2Âót°Å† [×K¸²:€ß=¹•Õ‡¾½l%ÇNz°z}Y›¿_Ãâ/JãFw3‚,s˜9æÏsF·ØmvR¡ °"o =Ò%¸œBýö|-~GŽêy«k4üñoøÓ«•p{ô÷·°(ÐÔªôP“Ôªu!„ï"&»¢LXûwA%%Jp9EÔÔ*øfG%.¹¨‰Ö“JK‘õî*Ȳd4íì_QZŠdÜ—¥úsÅê7ìóû¶_\¢DÝŽËÉ£O CŸ€é-dl©aa… !„ö“-6@H«þàœí÷ê+2·VbþûÇpâ”gJ}X»©o½«_Ö!'Û Ø¾«ÇNxpâ”ÛwUæ8[¼/‡]€½¾•hçžZ”–Z5o Ö¸Ó¿Ÿ>)^Ñq?JÊ,_]‹ÊjÕT&»¯Þæñ2\=Õ›¯sá¦k\6XFVŸ–å`Žw€ºƒÎr"„r®Åt‹ÑuŸS/KÇéb–¯.Á’åg°dùcÙà\=´Ìšž‰owTád±>±§g¯´³¦g6¶é‚ÀaXžë7Waþ§1ÿྟ÷F¯Lù¬êž?Ø^àPY¥âÁ'OÃfå”( ¢ª!Ü̸܉í{¼8uZÁ}—"¥;Ê* Ãþω~½›nµá…D€wœÓ°I!„Åt°10œ³pÃqÀoêƒËÆ¥`Çîjœ8í… pèÕÆÑ#õñ$v›€§?+×–¡è˜Œ1ôéeÇø‹S`‘õ”“Û߉kfd¢{·†à‚á‰ÈH“‘ÕÛf<÷Óï÷ÄÄK’°ï@•!=E‚ÕÊcÖ´008ízÐu¾ ½{Êèß×b¬{ù¥‰¨ó¨èÓKBNgŸ‰Ukkáh¸d´…E>”–+Ø_/cµrxäÞd¬ÝäÁÑã¸=ó8ädIÈËQßkåÀ ‰à SÇ!„Ã1Æ*$vtEZ*89_ €ÇãAuM5äª+aáw‚Hè~9xÁVßšÓѵ†Õ bæûÆ)ë!÷C.MnÜY7Ús-Ú^ ˆ‰àÀ7Œyjr{0ýk³'ÁéJ‡ßsžÚíW¿—¶çA!$ÅG‹ é 8Áž³ŸƒY„!„æQ°!g…ã,à„pOãi!„tlHëpxÞ ðV£‹‰Bé,(Øt„FGàvf<8Á޳!äD{B!¤S¡`Óµ*Xñõ×zª¿„D[´ÒÄd°#„ (Ø@@aØwЀa@¶Y–êúM¥B!]ÌÎI_ˆÝûjQVîCN–6¢cn0$×ر» ýz[árððù5ì?¤Oü7h€ RýdÀ…aÏ>7Δ˜ÀcPŽGCve 8q*€ƒG¼`CjŠ€ì>2gÁ¾Bo°ïpÅåÒS$¤$Ó!„ØAG­ãNÔÿpú¿'‹k0çå#€KƤ㫵Åà8`ÊÄlßUŽW_߇ꚂÝ¥§ÊxàÎld¤ë3¿úFÖl¬À ðú%¶n¯†Ã.àÕ?üùÕ£Ð4†{Öç q ²JÁŸþzðüYHJpì„/Ï;…’’€1¸×åð³¦bP޾¯?üå4Žó×_DŠÀcúåiÈlÁ¼4^é NfMKÁôËéêÜ„Bb›Æp±>̈ú|-ÑŠq"¸ú«MîØ]…Ù3³ÁóÜ{ý jk5Ìš‘ñc3±ð“#øjíiÌýw ¾/ßì(ÁšM5Ìš‘é2,<Þh•D!úu#þþV1JJs¡ W_‘„µ«ñ¿%˜û¯2<ûû,ì/ôãèq†9IøÅm} (ŠŽ{ ò@L ~pc¼¹@7·ÌÎ@¢K@f†uQßÐB!‚M(N8‹~‰€ðKc·Ào€‘)€»+P[Ç>¿†e+OBÓôCÿ¾Õðø€}ݤ¥YqíUÆPtX´ô8À àøDZ}ÈÒô1/œU¯'Ô_#Ë‚Š*àäi €A°aÕ:€çAE%p²Ø‚Ì ;8þyðÚ[Ç00Ç‚|'zeZÀ ÔpÕñ¼¤¦ˆa—@ „B:? 6ÞvÖa&TÞà†n›:wÀ¸ÿíŽ2ã~f†~Útu¯~Uí¤n WëϜ =XÇCÕ¬àxjjÀ1p‚ 5î€tì/ôç}cÈL×CKGA¿>Vüôû}°vc9vï¯Åî}uøpq1®˜˜Œë®JÿN¯›Bé,ºn°á$€³ãZÞÝÒYjè®ê‘¡üe øÙ¡V‚ñ¸èX ÒSmHKÕÏ::\T‹:·»UÀÎ=UÛÍH³âäiŠK|` X¿¥,b¹ pPU†«¦fàâQIÆe­C¿Þ6x}*Fœ—€ ÏO€¢jXµ¦óß?‰[«qÝUé°YypÆjêT¤¦tÝ B!±«ë½8K} Úu7}z90æÂt¬ÝXŒ§ŸßŽóò’Àq©†Eðì£#0nt:>ü¤¯‚{Ú„Ä啾ˆm?, 'O{°à¿G±è³“¨ó(¦å™Ç•S3ñ¿Å'ñ患ظµ»€¢cœ:ãż†aÿ¡:üó£˜ã€ÓÎãÀa7`XžÞeµòmþCnÌy©‰ "~rkú÷£S¾ !„ÄŽ®l8 àmh¬Vƒs»é»ëÉúé!»Ÿ ¶”`ïÁ*X-z÷t`ôÈT@‚KÂïï†wÞ;„’2/r²œÈÍé×笯3\1)lÜZ†VLŸœ‰>>€AõV¢™Wd"=Uƪ5¥8zÂQà–bÁ¸‹ºR“e àĉS^Ôºd¤É˜>9“Æ5tŸ}ÿ†LlØZ……n¨ƒ(]× $&„ÒQ8ÆX%€ÄŽ®HK1Æ i<ªkª!W] ¿ @B÷ËÁ 60œ-WxG›v9}w¬a`.c`h¸¿vÓümî~Øí"þþ— ŒKD–¯ßNýsá÷ƒ]Q¦åÆ`àðí!².áÛfQÊ0ó¿z‘ú­E©“±O®¡À`³'ÁéJ‡ßsžÚíW¿—¶Ç›O!$ŽÅ|‹ Çqú2ðvpœå\W‰´RCK—Ö‘Õ „b:Øp0@ƒËxNSÝà›hb0ÔôëíÄ÷fgA:Ën X$Šú8U­5žã…˜iD$„Ò‰Äl°á8N¿ñül lÜ:€(ƒ('P;¶‚g)3ÆÌtŒnœ.@’õ3ÈÔ@¥þ'C° éÀB‰U1yL}¦_ý Ïs<®á ð×ßS—]ÝÓÊý7W\–íàyŒiP•j€hÉ«ŸÃ‡Bi˜ 6Á¹VÎhµÑ¸¡Ær5PMõ`àX]‡Õ“4ãx82~ïQƒ¨`ѵ"„Ëb.Ø0ƺ¡8‚ € }áQsŒ2uUëëïÀ17èäãN¤~¬7Ï p%f@d¨J|u{Œ"rÂ5W?B!1-æ‚MÇqày¼ÀC’$”ø~ àU©†·î@}I/8V x…mÒ¾šî|âyV["’’³`±¸À˜OÍ7`L%'ÞÉ9å\T”BHŠÉÁÃÁÖžç! "DIäá(®½ÎùoݨJì ú5#YMýÚ:OžcF †)tq!Ë{Ì´šùNp !¡‚…‡¸F³à½¦Ê³ÈÍ…-¾fú¯ HÙX¦ªà©ùšª6¼˜ {ÆËá;"„BZ,&ƒ Ç‚ÀCȲ «ÕŠòÚÁáÝ—uÀ€€ï$ªKK`s …(§ç-ÐÏ–êø3¦¢¶k4ÑØÁ5ú ±';z”qPd`R•¼Çáó6,çDØ{Ì't?×$„Gbnæaõ³ 3¨ª ¿ß¯×ƒššZTTV ²â$’Ä¿¢wÒg?¨r¼ AìVpÈ9Åés iJ%3Ÿ­ÆË9°g¾É1¦þŒ7B!äìÄl°MÓ ( |>Ün7jjªQQYŠŠ p¾µÔãU8,eÍl´;.ôδ@J¼Ö´g Jú$‹l!„|1Ù<å;8ÎF’$X¬¨ªª¦AÓª«/Ææ¢A°rëá ÑQ„”„ã°Yj›ßi;!¡Fc24.‡C²„ìœÉ><¯_˜”B !„ï*&ƒM~º7Æôq6L«äÊDQ€,ɨ­»Åî‹q¤ÜŸß%€¢¨Ð˜¦—§3¥¾3ú Ð<ÏCàH’Y¶Àf³Án·ÃétÂépÂérÁåtÂf³A¶Ú I^Ïw–ÁÜ„Bb]Ì›†V‚Pß=U?t†çyˆ¢Y’aµYáõúàóyáóù i4M3Æê°ÈS~:¥£EG‘‘™Yî<³òÏPÞwI” É2¬ ,V+ìv;ìv;v;,V+,²¢$Bc]j­!„Òb6Ø„â8¢ØðRx^?[Êb±Àf·Ãïóé¡FQ ( 4U…ÆÓÀX}oI Wç·ŸüäÇÈÈÈèèªèêOo˜0xA7² Y–a‘eX,VX­H’ ‹E† ˆxÁŠ!„¶ÓÁ&t¬ ˆ¢hLÚ§Ÿ.Áb±" ô.(U…ª( ­5hˆe¢–NÇ"[Ð=)©)©]]Hk €ãyðAÐ[nDQŸcH–dˆ¢x¡¡¥†Zk!„´µ˜6€9Ü"xއ( $ ª¦BS5¨ªZjô–&H–etë–ˆîÝ;Ù\/§³áôë?ñ<_ß-%€ç‚`<Çó<…B!í&æƒ ÐppdŒN^àÁ4‚ ·Î4Œ©‰jbãà*I'œNWGW%Dp‚½`¸©¿ñx®!Äð<®þ±^<6ÞsB!±%.‚M©õÏL¡GïvÒ¯ +†Cñ<¯[±tÎ CÃJx«Lø2B!¤=ÄU°Ì­7áÔÐ0‹Á&8o ]•¨š /h!„´·¸ 6AÑM¬·W4¡y_bñ}&„»â6Ø„Š§ƒ+"M!„4.vþô'„BiB!„Ä 6„B‰l!„7(ØB!$nP°!„BHÜ `C!„¸AÁ†B!qƒ‚ !„BâB!„Ä 6„B‰l!„7(ØB!$nP°!„BHÜ `C!„¸AÁ†B!qƒ‚ !„BâB!„Ä 6„B‰l!„7(ØB!$nP°!„BHÜ `C!„¸AÁ†B!qƒ‚M'ƃªª.WåÖ†Béü(ØtbÇáÉ'ŸŒnÖ¬Yƒ¥K—v@­!„΋‚M'WTT„3f˜ÂÍW_}…)S¦à¢‹.êÀšB!›Nîúë¯ÇŠ+0}út0ưqãFÌœ9HNNîèêB! Ç«ØÑ!Ñy½^dee¡ºº6› Ç¡¬¬ <ðæÌ™ÓÑÕ#„B:±£+@šfµZѳgOœ>}Œ1¤¤¤àúë¯ïèªB!uEÅ€n¸‚ €1°X,8ÿüó;¸V„BHçÓ¥‚ c¬ñ[ýÿPcLë4·™3¯FJJŠñ:† Ëïtu Þ€†÷²©÷›Biqßí ú\,d³³ûÃjµл¦n¾ùhZg­wd½8Ž‹¸ÏÀŒ¢¡Ë !„ï"nƒMxxÑ30V¿ŒÀb#ØÀ˜1cPTT—Ë…I“&59q_g£€ÇqF Þ~p!„|Wql¢šàMÓ4ÓýÐ2¨?¦vÖCëìÙ³ñþûï#11v» ~¿¯£«Ô(r'ZáyØý´jºeXÜ× ã¸ÈŒEΟZv—E+Ë¢¿†2áËÃÇB7lE]ÞäöX”]‡½}ð/À‹€(sm¬.}Îs¡¤ÐƒšR?æÅÒ“µü³` ÆÙQ‚Ðp¦…B!­óÁ&jE?À[[ï‡7P €Áæ‘ÒOŸ±WŸŒ¯C«Ú <а Á¡‰²M›Æ¶®Z´ýˆõYäö¼€ à{ÙÁ!­¿ > _ŠâÀl*^ˆÑüõD’$Aõq6ÁÄúö(àBi^ÌöÍ„ÏY£ª*•lÅžÒ5ôÖ‚ŒöúÂ(Ôœ¥X?¤3 ð”q`n½õ,#Ç®ŸÖ `§ûcÔÔÔ ¶®n¿Š¢suÆSï !„tN1l˜'àSU,Ý\‚¤^Vbýu‰è¸Ø)Ø¥nàjÀ²GBª ð ¥µÇáq»áõxà¯6ÓLã¨!„æÄn°áZm4MC@  ¨r§±Øž¨÷²µ×11àf¨+ÑÚgã!|µ îòÈýøëjŠÛnÿu¥oûY°Câ-ã€:ÀüŒœöì‡Ûã†×çk6õ-6Æ…3)àBiFLޱ ï†Òê/—p´z‚ƒ<äàéÜ->š ª~†ƒ+ü(?¬ ®ŒÁââ2@Dö%2,N§wpx—üÊñÝ_PŽoö¡¬PÅ…?Ô»ÕÃæ·ÜðT2X9ŒþIëö¯)ÀÉoýÈÈ— È \ß¾ëAÿñ22†JÍo$xMËâ9v)¡ájÞ½¥ÆêŒ2eÊax½!à÷Ã_?K´¦5\ç‹Æ×Bi‰˜ 6áô.)µþÁ³qš;6~döÕ2¬ýkD ‡þd8Rx¸Ë4œÚ¡`ßR†]gmÃÚ7-}ˆ„¤¾ SÉ~L&<à<«»^†Ý‹½HÎMÁ¦ÝpR¸ÐÆAEc€dáË'xÔ*øý~ø~ýÚ^!-¥pC!¤%b:Ø4\BAcc´ä4{ük:ì]ì/ÿ¾þJì) s˜oUôu?CÙAu¥Hé/‘jîéóTh(= "àap¦òHÎí»+4”T¡x©¤ ’ Zâ= D;‡¤>ŠÖûqp…©ƒDˆ xO²ƒCb/§wŒ®¨Êc*Ü¥*T8³;€Ä>Žo  .ODÖ8‹Q—ƒ+|Püh&Ø(d ©Ÿ`›âÝ ªOùÔGÄÁ•n¤1xºÞ:Űã*ŠT¤äˆ¨9£âÐJFÜl‡51úP-«è„,Ø[Ôm¥_C5_}]£–B!­³Á¦áR úMÓÚf m]™0À™ÞºqÕCfXÁ…¬Rø•û>óÁ¦h}ƒ§×°¹^Žn`ðt 2ÏkXm¼~cdx«4økò¯µé“àñ Wú5V6ŠáôNß³E “;ÙŠS;ÈlÑIHkˆ§RÃØ»œà8 ò¸Šoþ寠+ôÓ³Ol  ò¨Š±w9 Èœtzpp¥C¯¶E¼o…MLhñû§Ž g¹1fLCH]Q„BZ,fƒ :ˆ¸íº*4EߖЂ1´æÊ'¿  ò¸ÞÕp3x*4hŠ>ûnbO‡Vù¡úÒK°¸Ò‰=yZå‡âÒЦeÍÉÌ—°o‰e… ’³EœÚ€%C÷~­ÿhÓ‡Hz8c@BŒ5§U$d (;¨À•É£üˆ~鯋“Cé%b;"/Ã!%µzÿÆœ5LoÁé ³CB‰-1l´ù G²Þìâ.ÓÐCh¦tƒ-ÿò ®TEæP Ýz ðÕ2ïV ød‘CÞÕßÀÑìúÈ‹Œ¡ò®¶B²qȻڪ/ÛàÇ®<È*aèÕVÍŒ_ôÖ³@ÂñÍ$g‹8¶9€Þ#䳚ÑÏÖ­a%Ž×`+~ý±¿ŽAÓJöéA&øŽwÏ6ÿ‰¼ §Ü]?­»UXÈ=†à  ñ5„BZ#æƒMP[E+‡¤¾Žm #?²Ù†i0u9úÀáâÝ\ôSº÷ÓÃБ5~SAâÐw´Œ¾£exÊ5¬ÿ§'¾  ßE²±¬Ïhîr çºqòÛz’[TçÞÊXóJ-JöI¨;£¢G½Ñ²¼Ðð:ZÖÄÁW ¹²aÌ~§¡ŒÈËpJÝÁoucK°+*üF!„´FìNÐ׎ò®¶¢ä€‚]yá«Ñ®þ:†¢u~ìùÔQ^”8Ø“xTŸÔ»iªOª8º)`*S¼[ÓÃââÁ €Å¡·JïQŒ aqòàx@v´¼ÅÑÌ#©¯ˆíÿõ =O‚lo|]ÉÆÁ‘£¬PiÕä…=GȨ<®âä·(>}Å€‡¡ò¨þ¢dÁ—œl>­›B9Çâ¦Å¦-%ö0ö.v~èÅò§kÀ‹T?C·^ψ2‡ äN±`ï§>\ƒ¦Z°c¡^ì IDATjٻċ­ÿÒ'ÕS} )D£Ehï/¾YÀ`Mà øRsEdäI­jõès¡„o(è5²ùVžþã-8ºÁ½Ÿzѳ@2Î|jJRyWY±™û>óB´qPý@ßQ22²a@ƒb!„t4Ž1V ±£+Ò óרðxܨ®®ÆïWE] ô¿PIÑ»\Z~Öýl!‹“3ͽbzY[7>ê—€‡Á_Ç`OÒ[eB[Lu >7ƒ½»>i]˯¨­?>²Îã›ý¸øÎfËš³†§[xµpo5Ó¬ "œR7H‚µñ«{7¶½´bp ½¦á´ëV^õ U¬ ·”-`M!„´7 61®ô ‚“ßs™©¹íÿqrà`°NèW"m÷]B!-FÁ&Æõ,Ðs¸>V§½O"y ìR"Nl÷}B!gƒ‚Mñ]º¦xN„UtAæ#gn—B!g‰‚ iÏ ° Nȼg5ã!„rŽQ°!N„,8 óvpõãh¨ñ…BH, `ZÐ ,R´ÎK‡ô< o…Ep@â,æÓ³ !„AÁ¦‹«:Æ¡[š‰rJéÛh!„Ä( 6gžÀAÇñà”RP}JAR_ E›<`Ãy×Ú¡(­v£ò˜ Az —6°ám×TàÐJªŽ«p¤ è;ZÂá¯ýÈd`áP¼GâaèYÐpݪS;à8 =Ïü\É>ŠHÎÐû‚† az«4ïVP~D ±‡ƒÇ'¡tƒ¯´ n†]KªùW´þªÜ„BHgAÁ¦<'Bä$œ “ÀsbÔë!yNW£p¥’¡ïˆ8“EØy¾œ{šbAß‘vxkØõ~-0]Bï2T¦bã;U¨8@ö8µe6¿éFÍ ý/±@°å‡øªÍÁ¦ô€^àŒ`³ÿ Nn  Çp ‡cý¨:®bè56pà±û#lN9#’ÀCDÉ!,¼¶$+‹C@bfË.¸I!„tfl"px $Î ‘·¶ê¢ŽÞ—þ4 ézà8²©U§˜ñpOHV};V‡û>¯Â€ SQyÒÒ=5˜øË¾Hê%Ae*v.­ÄÞÕ°ðvˆ<ƒÀ©à ‚çhŒ`ú€^è-Gž àÈ׌ú ÉÙðœ€ýU/—C¸<6§„ê¢cõ«tØ»‹zä9$õ²À–(•*¢Oƒº !„Ä< 6õDΙ·Câ-8Û‹žÛ»‰F¨€ò£~X{ï:§×Ú?ýŸ$ÚÑyl®Náé Z9§ßý4R…Öu“¢Ðà¤4ƒÍÇvXs¡¶qd°\U_ôÀöZC•böŒÂQh°-U®t ´®~Å{ºÈ¯Iš—®ýµ^Nî/qð™)ý§d»nþHÿ™æ7!„bÅ:/ƒBãY\YRSÓRú>=ÅÚézSP1Ù›âXÄø‰€|o~Žï+Î:ÆÑÝmÆÀ‰ý%/NfÎ÷;D¡!U°é½Ào9GsÐéÝâÓ³Ågç»:¹ÿÏqê•=[|_R›B±:œgÿÍ®ðt–¼Ó‡§sç4Ôl¹.‡—±xô §8ôì4“C!Ç÷ùÅ÷“¦©ÞÍ>k]žø÷!N¾TâåŸMràñ©–c¬ÙžbòTÀ¾3|°Ì“_„ Ò1à2xIšg¾1ÌKM092|°Ì ß%,ÇLž ØóУG+ÇB޼0¶k“jNï)N¼Xäè/¦9õJyÞ¿EæBÑVEÅF)…R ßz]·ò(,ÎEï‘tÁ¦s]ëÈ"ÛSÜú±~ž¿”ç¿l·Ãú+ÒÕ ‡7ýfO~y˜ŸÝ;DnÃîéâáϯÃÏZ\ÿ}¼ôÓ ^þù$›®Ê’{KÕ”ÉÞøÁö><ÎÇ'Ùýà~΢wKÒÜe»šñã¯>9Ez6z¼ñƒ½ØžÆƒ§‰*1Ÿ™¦2qÓ\sÖ?›ÅPªñ=6ÿ!„KÑöÁ¦vó›/ªØÊÅ· Xg¡ÍBÖ]žfÝåéYÛýœÅÕïïnÝhZ_¿áýõ¦£Òd4냧¼8ÕèkS}¬ý®-Åö[ l{k¡åðó×| §Ñ45ã–«ØtM–M×dWVoáj¦‘p#„b)Úº)ªqÓSh¥ð­&N~Rö¹5¢Ébrˆ¡4„Sÿž㣵®ÿ¨¦I¸B±Xmlê7;•4c ™‹ê¯¥¤ü™p®îÌÙŽfÛ[ò³FG­&ÆNúò”§Õ©L4€ÖªltkS”„!„‹ÑÖwO¥ …RÉÍp0³£þZq" 0ÓËuygÄö;ß݉í®Þ¹I%ßIq¢:sf-–ecYºú}ÖúÙ˜™ãÝ…Bˆy´u°1˜úÍOkÍúü%õ׆•0BSž#ܬÞаhËôÄþÚˆÃØ‰ Vì‘Õ}8ŽmÛØŽ¥­jõF: !„X¼¶6IµF¡-mYlíº†noq`8ùr2ÿK)£b¦f½»½^Ggét&5…ÊOpü¥iâê°õžâåx®‡ëy¸Ž‹c;X¶U­Úh!%„bÑÚ2ØÔnrÍÕ˲ñ\Ÿ»6ýµ±Ðc'*L  ¥xŒéxˆŠ™&2+kPûYTÌPìRELÇ*?‰R0z´ÂÔHÒ å6oÃO¥ð=ÇuqÛ²°¬d&gÉ4B!kU ÷ÖJcÙŽã°¥ë \Ýqü; 8¶ošÉa‡5[R„V™ÐÌ? ÝŠ¢f<¶9DáØþi¦GÃúÖÍãwËvIgH§Óø¾ëzض]oŠª}RµBq:mlš+6Žmãº.¾ïsëÆßæTé ¯C“CÓ£!¹‡tÞÆËZØŽ^5¡aN+¤ VbJ“ʼn‰Sq”\˜2šµã·Ðï_L.—%›Í’N§I¥R¸®ƒm'‰kMQB!Äb(cÌ(P8íž+1c Q!¥R‘©éiÆÆÆæÑ#_âѱÿEhŠ+å>/¿ÒÍæ‰»èó·ÒQè ³³“®Î.::;é(Èf³ø¾ã¸É()4-JÀBq:m]±FS”ÖÛvð=0“! CÞØÿK¬u/ã'CÿÀ±èÚkè÷jã†yºK—±)| Ù\žl6G¡P ££ƒ\>O6“i 4v}È7H¨B±8m_±©=FQDE”ËeJåS““LLN211ÎÄÄœšz•勎Æ!q·Cœ}6Ùx€œ$ep]—T*E*&›É’ÏçÉe³õ`“J¥ñ<Û¶ÐÚª÷±‘`#„b1ÚºbS›¼­ÖÏÀu]PIPQ:î8.)?E6›¥¿´…r¹La5Üèĵ.ªr^ƒ$€ ÕáøZaéd„“ã8õ~P©TŠt*M&›%“N'‡S)Ç©7?I”Bˆ¥jë`p£uÒØÆ¦¶â£ÕÔ©¸˜J‘.¥)—ÊaH„a£jÓaæKÿþ%n}Û­ttt,÷¥œ–1T«-`U‡nÛ¶m;x®‹ŸòIù)|ßÇ÷}<ÏÇqœz°‘&(!„g¢íƒ 4…¥AƒmÛ@c~Çqñ<Ÿ ¦„Aj¢8JBMœ³Â»9r”\.G__ßr_ÊiÕ±LÖ|ÒXZcÙ6Ž“|nµzãº.ŽãÖM-ÔH Bq&VE°iVk¾°ídFâæ&Z•&Šb¢8jTkê}mÔŠîy]Ýôôô.÷¥,Ì$ÿ§T²B©®O¢˜ô›© å¶«ADzìj“Uë,Ãn„B,Õª 6Í‹%ÖFÓh£ÑJaYÛ¶‰c—(jšØÄõjM=Ьà&)DZÉå²òùå¾”…5¯Ê"É7…-k}hjÍó5!¡F!ÄÒ­š`›a­Cqã†iêaÆvì$Ì4AÕöo‡`cY6¾Ÿ"N/÷¥,¬JLí×F¸i<ª–ÎÁR¥Bq6¬ª`SÓ\½i8õ­†MfåÞXµÖÕ~)Þr_ÊiÔMK¸™ù83ÄH¨BñZ­Ê`­Õ›Úï3oœí0ªYcÁOk¹/åŒ4>ÿj¦YÛ…Bˆ×fÕ›š…ÂL»ÝP›«OíL©¶\T^!DXõÁf¦öíl„BˆsEþÓY!„«†!„B¬l„B±jH°B!Ī!ÁF!„«†!„B¬l„B±jH°B!Ī!ÁF!„«†!„B¬l„B±jH°B!Ī!ÁF!„«†!„B¬l„B±jH°B!Ī!ÁF!„«†!„B¬l„B±jH°B!Ī!ÁF!„«†!„B¬l„B±jH°B!Ī!Áf3ưoß¾y_Û³gÏë|EB!ÄÊ&ÁfSJñÇüÇ<÷Üs-Ûã8æÃþ0CCCËteB!ÄÊ$Áf…»ì²Ë¸öÚkyæ™g€$Ô¼ï}ïãK_ú×^{í2_B±²(cÌ(PXî sÛ½{7×]w©TŠË.» ­5?ü07ÜpßûÞ÷–ûò„BˆÅ^î  Û¾};ù|žƒ244„ëºcøõ_ÿõå¾4!„bÅ‘¦¨6Pkr ‚€©©):::¸ýöÛ—ùª„Bˆ•ç¼­Øc–ûíCúßþö·™œœ ŸÏÓÓÓÓV$¡…Bˆs鼨ØcZh¯@pË-·Ëåê¿·kµfÖ÷ÐfÁL!ÄÊ·ª+63oœµßMl@ÁÌ|³Ro´¾ïÓ××ÇÑ£Géììäýï?Q-÷e-hvuÆ|èÉkµ×kŸ¹Ts„Bœ «2Ø4”æÊ@Çm0󇟕æž{îáÙgŸÅó<.»ì2‚ XîKš×Ì¢”ŠF ÑZ×_SJIÀBqV¬ºáÞÆ4ª1q׃MÇÕŸˆ8®n3q½jSß µoß>nºñ&®¾új¾ù­o.÷圞i„¥@)Ö ¥4J)´Öõ¥UKè‘p#„âL­ªŠMsSS-ÌDQD…DQò<ŒBâ(jc ^ÕY¹7Ôþ5ý(¥xûÛßÎÄøÄr_΢(•üŸVªl4–¥ÑÚ¶-,ËÆ²,,ËJN5ðc$Ü!„8#«&Ø477c’†„QH¥RáÈè^^~†£Ïrlr?Q’¼¥Ú$ÕŠ7_UàØÀwøÔ£-÷¥,‚B®•f sër—°!w)…L/ŽmcÛŽëà:.Žã`Ûv½yJÂBˆ3µ*š¢šCM­J†I yâà·¹÷¹?a¸x¤ùËs¡¯QqØêjï›ýæÜ¹sÓ¥?¿ ?•Âó<|ÏÇu]lÛÆ¶-´¶Zúà!„‹ÕöÁ¦ÞüT­Ò$¡&`xâ_|úOxìðWª9¦f´¥pRz7<­J1qØ$åssßG¸níÈf²¤ÓiR~r\ÏŶ$Ü!„83«"ØÔ~jUš“ã‡ùºƒáéõ½ð²Ýë<¼ŒåÈòõ–cJS#GÊ”'ÃÔ/ÌÜÌ=[þ”l6G6›%“ÎàûIõ¦Ö÷F: !„Xжîc3»ZR *ü¯'þ j”2ônN‘ïs—óR[ÍÓf±Ï|o<íîgóxó¾hZž)@Û ÛÓd=M¦Óaäh™‘ƒ%Œ}S?äÑC_âêþ»Á˜dd”Võ!àµÅB!Äbµu°FÅ&Š"‚ ä'ûÿgŽWW½V† —çpüêËɨåWb>ã÷¦çõp1ç¾fÁ}gý>×¹Lë -ŸËéŽ×|ZŽgæ¼^ËQ8…ŸSt z¤s6wM¢ <2ò¬õ®@«Íh˲-,])Õ˜óF: !„XŒ¶]R¡¹ZSë0ÂÌŠB±T«¢b³D ½Uæ¿)ŸØ²çÛå×v¢Eô…Ù÷ý2Çw…M×f8úlÀó_)òò_ãùçº$¥É¹]¸Ö™‡!„âl`s–GcýÜűùƒÍôpÌð+Ἧ/™üÙãG£–Í¥±˜ Øþ~ðç~Z¡û›®Íg·Hg)›œÛƒ­½³z\!„âLHST›+OÌŒ)|vÞ“tÜ­E¬“{CÖ^å0p™sVÛ«<+MÊ.TG.½ã !„gJ‚Í9tìùÝ÷—¨Lz/´É¶ÎébbxñÁ2‡Ÿ ¨Lòë,.¹Ó'Û—Ò^ùI…ÒXŒ“V|,ÀÉ(Ö_ã°þêdõã_˜àÙ/¢E÷‹mïôyþk%:7X ^éðè_O1yš!Û§yñÁÖþ-»ï/qô¹€ïö¹é£ k-ûÛ)Âr’Jã1¯þ, 8j¸ú?¤Ù|£Ë®ûJL%%šKîòØöNŸ7üjŠÍ7%ÍAÅá˜ÊTrŒ+>"Õ©Øp­ËL±öJ‡°d8ö|P¿Žò¤áð]›O?™žk¥È»}I¨B!V 6çÈ«äÖX\ôT§æÂ·yõJ @X2¼òH…mïðYs‰MºK³íÆÐÒñW[$Uœ^ÍÀå…u§^L^O’ãy9EªCãff÷,ö m)¼t²å*Ö_írè‰F°9òT@®ß"×?°ÑÊ"ët‘q:ÑJþÙ!„X™¤)ꙎéÞÚz¶ÚõÎÃS§bL{¿Wfï÷•œ8LÞ[Ó¹ÑJЬ6ùd{-&O¾¶u±¯tØ÷`™ñ#ù~Í¡§*l~ÓÜ•ÒøV×Ê,jͧÆ‘f*!„¯; 6çˆå(‚éÖ;{¥éwí$!aÇí™n ¦š Ø~#@hëì¯“äøŠþKm=°f»EX„þK[ÿ)(4žÁ·2(£%£!„h lΑ܀æÕŸIZ©f“SûBüBòK¶WãfC/Fôlµ[‚ Í QàçuK`Z¬õW»<þ¿§)F \fc9 L2|Û³28VedñI!„íE‚Í9²ùF—Wð£OM±îj‡ÃOXn#(( —ÞíóÌ¿–;±f‡M&ágë-ë·*vÏ…6û¾[fä@Dn¦ÿRgQ“ø-2Ýš¡ýÛÞžÂÕ)\ÆÑ^#d !„mF‚ÍY⦾ÍÃM'©ÂI+núý ¯IÞÚ+–ªËúkÜj3“‹­“Sv8öÌQ®xO7‹*ñ!„m@‚Í<G{XÊ­þØ,9Ì·»1IЉ£Fè‰#"ÖÐk»v…V6JÙXØØÚÅÂIRáà3§è\ë2°#={im!„¢MI°i’„G§°•˹«d(46Z7U_Ts±Æ`âˆÈÄcHãZ©>“)ùQ€F+ Fµ{š*8ƒ—fXwY†|ÿégB!Ú‰ÀVž•ÁV+¡YF¡PªÚ—¦)˜#¦æýý4:׺§í ,ÓÐ!„hGçq°Q¸:…§3hå,÷Å!„â,8ƒÂÓ<+‹’%„BˆUå¼ 6ŽN‘²r(7GŒh8×MSJRªåG!„XªUlNw#´•‹oå±T­³¬ôin„B,UÛ¥TÓÀ¢äFØÜ‰6muàèôëaç«Å”v”A©ä{ª}WI‡éÖjTn„B,U[w2©ß«Ï;½uÉ ¢ yªUÛg·ÕÇ@A¥×CuÕƒŒV­ÿ4%à!„X¬¶ 6Í7:­“á`v[}[e:™ä.¢òº_›XXì”(M6–ÈŃX–…eY(­[ª7F&B±Hml µ³©Öšuً믟JM9žÄÏ|çëx•¢…ŽQÙ"S#IYM(°Û¶«?IÀÑMG!„XŒ¶6Æ´V¨jÓÅ–Ž«ëýl&NV¨“Y{‹ñ(³;~œç7Ëåøó•!Π4L„L$?ìÅwr8Žƒã8ض¶´Œ’B±dmlš;—BR­±,›¾ÜF®ì~O}¿Ã¿˜ÄM‰©ø‘ f©^Ggãtvˆé<…öCâÐpü¥éúKë§Þ†ïùø¾c;8¶ƒ]k–’@#„b Úºgms3”e[¸ŽÃmÿ^ÿ)ãÁq¢Šáà S¬Ý–'`ÊœD+‹3\Ôòl™§ËÈœ›Õ¯«êo3ÞÐÒeŽƒ™ÛÍ|ûÎu!3O7ç¹gÈX!ÆPv„"é0|lß4QìÛ5¹“5ÖŤR)|?…ç{¸®›TmªMQµc)Õ–9\!ÄëhÕÛ¶q—|¦‹w þŸüë?ÀSž yùÉqz7¥È÷ºÄ:$&$XéýQçÊ\mZ¼P€‰aôh™¡C¥zsÂ<ï&Û‘%“ÉN§Iù©z°±¬æ>6múÇ !„x])cÌ(PXî YªÚH™8މ¢ˆ (‹LNN02:Ê3‡äcN1nÌs£ÀMYøY ÛÓH+ǹe „å˜ÒTDe:j©(eŠë¹¨x=ÙutwuÑÕÝMWgäs9Ré4žëbÛN}è·4K !„8¶­ØÔ†×›¢, DZñ}ŸL:ÃŽ7Ñkoæ{Ç>ËË•cHn´åéˆru(¸xýéØapâͬo —ÏÓQè Pè —Ë‘ÉdêÇvÐZúØ!„Xš¶­Ø@­jcˆcCÇT*Êå2ÓÓÓLLN0>6ÆøÄ8û‡žà•É'8¾Ä¸:HYM,÷¥Ÿ7”Qxa7é Ÿ\4H_¼“¼×K:“!—Í‘Ïç)ä ä yr¹<™t&éDì8ض…Öɺ^p„B,FÛVl Vµ­“›žmÛcˆMŒ!™¶_[ÛíëX—ÝÉôôÅb‘éÊ8• L…ÕP”4“YCê¬I–H [»ØŽ…rp]ß÷I§Ód³Y2é ¹\Žl6K:•Âó¼jÿ«ÞYXBBˆÅjë`ÓL)…eY$#süêÔü ˲q]Ï÷Éf2”Ê%Êå‚ Š"â8&Žãzõg%wR'Jc;+ýk3 T}ý§ZçnÛ¶q?åãy>©TŠt:M&ÆO¥ðÜj¨±­j§áåþ;„B´›•~‡<­™}mÀNâI5ØØvR%H¥R”K%*• AAjLs°©k™ÿ¦ù|åË_åïx;ëÖ­[îKY˜JVÂL‰®.y¡’‘kÕïÃó=<ÏÇ÷<\ÏÃu\ÇÁ²­–µ¢¤Z#„b)Ú>ØÀìpcc׫–ec;6žëYzoŽ IDAT¤R„A@†„MÕŒ©v.^ÙMQéLšŽÎNzûú–ûREJkt}®¡ê’ ÕïÄu\lÇNúÓXI 5B!^‹Ul 9ÜPŸØMë¤yªÖ†!qEqSTÜ6ýk|ß§/ÐÕյܗ²(µ~6Ji”®†maYÉLÑÚÒØ–]_Ä´yB> 5B!ÎĪ 6ÐèLœÜL“£ÖÔ‡ƒ×;Ï 4fåWk<×#•J‘Ëæ–ûREU›¤jË^4&TTIuFϽ”„!„gjUhTn 6ZªÑÕƒÁ`b“ìS»ïÒÁƶíú¨¢vмBí{¨WpšBLs‘P#„âµXuÁ7ÇZVi¾yc0ªbVö(¨™jàÇYîK9c-U™¦`)F!ÄÙ°*ƒMMóͲV‘™Y!h'Íkc­ FBˆ³kU›fsÝ@Û¡ù©Ys3[;’#„â\;o‚Í\ÚíF+k…Bˆ…­’6 !„B 6m¥Ý›¢„BˆsM‚M‘`#„B,L‚M‘`#„B,L‚B!V 6mD*6B!ÄÂ$Ø´ 6B!ÄÂ$Ø!„bÕ`ÓF¤b#„B,L‚M‘`#„B,L‚B!V 6mD*6B!ÄÂ$Ø´ 6B!ÄÂ$Ø´ 6B!ÄÂ$Ø!„bÕ`ÓF¤b#„B,L‚M‘`#„B,L‚B!V 6mD*6B!ÄÂ$Ø´ 6B!ÄÂ$Ø´ 6B!ÄÂ$Ø!„bÕ`ÓF¤b#„B,L‚M™l$ä!„­$جpŸûÜçÃpÖö“'OòÅ/~q®H!„X¹$جp¼ë]ï" ÃzÅæÔ©S\ýõ¬[·n¹/O!„XQ$جpwÜq?üá¹í¶Û0Æ0>>Î7ÞÈÑ£G¹ñÆ—ûò„BˆEcFÂr_ˆ˜ßÆ9yò$k×®¥T*qøða®»î:yä‘å¾4!„bE‘ŠM¸á†(‹¼òÊ+>|Û¶ù•_ù•å¾,!„bÅ‘`Ó~ý×|>O†cèîîæÎ;ï\îËB!V 6mà–[n!“ÉÔ÷}ŸM›6-ß !„+”›ÈÓòãû>ƒƒƒõ×o¾ùæYû!„B‚ÍŠqºò|­5étzFÿ3ëýt„Bœ¯dTÔ2Z(€Ìœaxÿþý\uÕU8ŽÃÁƒñ}¥T}ŸæçÍæÛ.„B¬För_Àùh®@SÛ6óµÚï›7oƲ,ºººð<¯^™QJ›zˆi3µ÷KÀBq>`ó:š/Ð̬ÎÌ 9ÆPpÉ%—°}Çv¢(’°R 6 U:µG 8B!Î7l^§ 4ÍÏã8nlÀc/ñõç?‰¾î)v?Å_þ(ä=—|œuÛPJ·Tj”V(Tý3«8Í×"!G!Äj#}lΡÅšchz4ÄqŒ1†ƒ£»øú®OòØ«_'&ÂÄ ªÝ½Ы־‹÷\ò ¶t_Q8Z';Ì|œ«™ªFŽBˆÕB‚Í9°Ø M­:ÇqÜÑôâÉŸóõÝŸâé#ßMª6§±sÍ›¹ëâO°½ï´¥ÑÕ3+èT›¬ f púÈB!D»`s–,¦Cps ©=⨾m×±sßîÿÁ®?™u,¥]k=,G1|¸LXŽgísQϹsûǹbíÛкQÁ©5OÕÂNsÿ©à!„XM$ؼF‹ 4ÊLÜnj•š'?À7všýÃOÌ:–e+º×ûd»m´U J'_-QžŠf½gCáRîÚñq®Y;–e7…Uo²J~Ô¬ŽÆ5p„B´ 6g¨5Ð$M:§ 4ÍaðØÁûøæžOspl׬ã;®¦g“OºÃ¦ž/ ıi <3t°Äôh8ë¹ ¸cûG¹aãûplw€ÓÔùXŽBˆ6&Áf‰ì?SO¦9Ø4ª3ÉO–ùÉã›»?Ãñ©—g¼ŒEï&?kÕ»¿Q%& &m),'ù© Ë1ÇˌŸ ˜Ù-§'½Žwoû=nÞü«ønºÞDU :ÍMU G!Dû’`³H w6õMµß£¨5Ô”Ã"íÿGîßûY†‹Gf26Ý|Ütc• C¢0®‡•úeTûýÚžÂvt=E¡aôh…ÑcLÜzÍ¿Û.úmn½àäÜ\nš;KÀBÑæ$ØœÆbG8Õ†n×F7ÕÍTyŒï¿øy¾óâ_3^>5ã@ïsèZça»ÍÆGÉ<6µùl’ó5Þkª`yÛÓõf«86Lœ>R& Zÿ†ŒÓÁ;.üo¿è#ä¼®jõF¡µ5oc 8B!Ú›y,-дöŸ‰¢ˆ‰òìýÜÿLc-ÇQ@çZŽ~m7Í™zs@œ 6Êš^Æv5¶§êýpŒ©á¡ƒ%‚#©|;Ë-[~ƒÛ¶ý™5IÀQIÀMUp„B´ 63,*Ð`ê}išûÏÄqÌHñ÷ïù,½üEÊáTËq´VtoðÉ÷:õ‰öâ¨Z‰ 1´†˜ETl˜µ lWaû«)8ÇC†•)M´Ž¤rµÇ›6ÿ ·oÿ}z²ë±´n©Þ,6àÔ^B!–‹›ª¥ÍlˆMLU‰91þ ßÜóY~ràŸ âJËqlGÓ³Ñ#ÓåÌád¢je&9I=˜T]t°1MÛTõc’áâNJ·4u•§c†•˜iIe)›ë7¼—;w|ŒÁÂhmµ„™ÍX!ÄJwÞ›¥79EÄÆG1ÆÄÝÃ}¿øK~vè+D¦©RáÔ³Ñ'•·Z¶›8inŠg…•Á&fá`£˜3Ø´þ’6µf›–4½Çñ5^ÆjLøg`j$däh™ÒdkGã-]Wrç¶ñ†õ·a5õÁ±, -ëQÑòûLp„Bœ «:Ø´šj%cIËD!üßÜói^}¶åض£èôÈ÷¹-#œ0-3š‡jae¦¨YûžA°©Ulš¯ažs4×v^ÖÂñPi"bähyVGãuùíܾý£\¿ñnlË‘å„B¬«2Ø,¸Ž¦uÙcâF ‰¢ˆ0 xäÀ—ùÖž¿äÈľ–c»)M×Zl—ÓÜ…dv iÚnZª3K 6ÍÕ—%›¦‹™ëµÇZäÚNÚJŽ›¶ê£¸‚RÌÈÑ 3:÷e6qûößç¦MïÇsR³—k€#„âu´ª‚Íb×qšoÙƒJXâ‡/ßËý{>ËÉéW[Ž“ÊÙt­sIåí'hy˜-fvXy½ƒMí\ªõ³*7õjV²Ié$àø™¤/ $K<ŒŸÝѸÓàÝÛ~—·^ð!<'-Ë5!„X«"Ø,e§¹–=˜®Lððþ/pÿÞ¿b´|¢~ ¥ ÓéеÖká§4³Híº_÷`S;‡š¹okÅff³—Âàf-RY»Þ(Ž 'FVZf4Îyݼsëæmý&·0;à(êMU G!ÄÙÕöÁff¨™oþ™¹ÍDy˜ïíû{¾ûâß1Y®C[Š|CÇàŒN°ø@ÓôûŠ 6µmõT­Á¦6‚ªùµ÷¸)‹TÞÂöt}ß©á€Ñc•–ŽÆi;Ï­[ÿ#·mû-ò~ff£µ‚¹*8M×$áF!ÄRµm°™7Ð4õ¡™+ÐDQÄXé$ìù¾ôyŠádý¶“Œp*ô»õ!ÐÕƒÎõtÆÍ¿}Þ`S{úz›Úö¹š¢Ôì`Óò75_Wõ}ާIå“~85ÅñˆÑ£e¦F=+Í›·ü·oÿ=º2ƒ-ëQ-¶‚#G!Äbµ]°©ÏÃR½Ïßä4{§¡éÃ|k÷ÿäáWþ‰ *Õé¦4ý¹Þ¦% ~c/޽`ûŠT‡&? ±}Ußçø®ñ#¾Í›qÁ…ðè_OqÃïdfA¥hpÓª ö|»LÏV‹®-ö’ƒMcŸ%šŽ5ß9êïo OÚV¤ 6^Ö®v•bÌèÑ2§‚ú[írã†÷sûÅ¿Ï@~KÓ(*YQ\!ÄÙcŸ~—å7Wš¸idÓ‚ Sš˜ccûùÆžÏðÓÿNh‚ú1Ry‹Ž~LçÜ‚kO'OEüâ›%6Ýà‡pô™€‘¼ÅeËÍIñrŠlߌ~8µcĆÑW£9_=ñܗмéãÙú¶©S1…Áj%¤©iæœ:Í9j/ÏŒQ`˜809’*ØI'¥éÛ’¢kŸt4>Q!+<ôÊ?òÃWîåÚõws×Åe]Çv´Žç\p’ïufÀiþ· !G!ÄL+6ØÌfjÛ\˜2®.Nib^~žoüâ/ùùáûˆkã­d;:\ü¬5ãà--lO±ý]~ý÷‰c?ùŸSäÖXôn³ñr ¥Á&,Ž¿¢m’ÊËyoqÔc²ýÉþå ÃðK&‚Ü€&»¦ZŠaø•ˆâˆ!Û¯ÉÔFDÁØÁˆÜ€fä¥/ ùuɱâÈ050=àçlÒÛUt¯÷èt«3WË?=ø%=øe®¼»v|Œ z®¬/¸Y 3ÍË5Ô¶-ýpÕ; 8B!+2ØœÉ:Nq­REìz’¯ïú$Ïûµiê´Väz“@ãxg8©I®ßbÃ]=Ð»Íæäž‘—¿?EX6üøÓSøyE¶Ï╟Vf jz(& G#”†ÞíÉöU°}…ŸS칿Ìu¿“ÆÏ+‚’áñ¿/âÙ5š§¾Xfë­ý;g•¯tm¶xþ+¥9ƒÍBNü"¤ÿR›M79ÉŸZí3sì¹(0\õa ôï´ùÙß¼Ú®^ l~‹Ca­¦¹ϬHa’Y‹‹nÊ"Óiãe,rݹn‡âxÈÈÑ Ó£!ÏŸx˜çO<Ì…Ýoä®çòÁ[ë“üc’9tª·–€-}¢f†!!„çŸlæåÔÚ)8irj,yÐ4Oú÷íùû‡Ÿ¨¿?éì’ë™{ɃOg\Ìâ®9,œÔìéøÑˆž­f®ž –þ1w_Ðx~P3q4¹³Š(OžüB±Z!1”Ç QÅ4ÖzZ„þ6Ï¥ÌðK}[ \nc¹0~$&(žý×r££0P‰±…å&×3Sm×\WP™Ž¨LGX®&Ói“Ê7~*Ó1#ÇÊLž Ø7ô3þüÇ¿ÊÆŽÜ¹ýc\³þv,ËNŽžÑÕ¤¹Â#áF!Îo+"ØÌj„[úÐÄ1Qðè«÷ñÍ=ŸæàØ®ú{S9‹ŽL‡=ç’3žÎ¸¥]÷©C:ÖY³¶[®¢2Ý8X¥8ÿç»7W—TcðÚ†Þm®u[F,Íj,G7Í4]S®_sÝo§}5âè3!Oþï×|ÄG[PXkqÁ-N˨(;¥('áI%Åš9-pÂrÌØ±2'+d:ÒnZ³fKŠîu>cÇÊŒ80úŸyô# ¼°•;¶”6¾×ñêœæåŸQkc 7Bq~š»×éëháPÓXò Š"¢0¢\)òЋÿȽÿ&þ¿Ÿýv=Ôd»Ö]’aíÅ™d”Sí¾V»Ó¶>qó½0·ò„a×}%ÆÄõQQͺ/°8¹7¤2e01yª¹¹ªõD™MyÒ´¬¿´Þí6Ç_Hæ‰ñ² /«ˆCæL¬¤ Ì@P4œøE£9¬5‰núxfÁëX÷‡âHÌ?9Ev&˜NF.]öËþ¬}{/²8þBÈO>3ÒйÉ"’kxùÇÉu/«ˆÍovÑv²Ï曞ø| ¿ ˆc°¸òC³܂噦]˜±‹k¥Èº+Âq‹pÜÃÊT° e´Ñ1L’89”t4>5}ˆÏ?ùßøÊ®¿àÝÛ~—·oûOøN0hm%ýoª'ÊBœß–u‚¾æÿªžY©©¯ãŒóÝÝÏý{>ËXù4Óú] }nkÌŒÜp¶šœÎTP4( –Û8çÜ“æ5š|Z&Í«ï;{‚>AiÂऒàaÌÌ÷5ÞL%MIõcUÏ– a ¼¼j8°z¬Ò¸ÁòÀv™}]3ÎAcÓ‚¦ì )»Ðú4]—凨… VºQåš =Zaz,©Te½.n»ð?sÛŽß"ëwaÍ3ÉÈH)!„8߬¸`Skb˜( ñ]Åwöþ S•Q_Ó1à’ïqê«M'ožqÜyOxv¯ÿôæ¾¹7½´È`Óø¬æ›x¡`Óz®yfžlfœã´Á¦)¤ÌlŠ”SÀ³Ò3c-ª9Ρ»£Œ­Ô«1åéˆÑ£&‡‚jHÊòöm¿Éÿ>é5-sßÔH°Bˆó˲›™C¹G¦qßóŸæ;{ÿžR8€ŸµèðHwÚ­mWr izz^›zXiKÙdÝ.4ά÷4>§ÖsÔ-ƒS(c*(l +1£Ç*ŒŸˆ#ƒk¥¸åÂq÷ÎOЛ]/U!„8-[°™Y­99ñ*_~öÏyðÅ/DÉq™N›Î?7÷ Á§Ù´ fŽ“®à`Ó|¼³lª×N“v:P¨y¯KÍqŽ™çRÚ`å+8´“ô´Ž#ÃØ‰€±cÂJŒ¥Þ¼åƒÜsù°¶cký+`#„ç³lÊå2ÅbqQû6›©p˜?ùÎ-ŒOI…fÍ©9:ϽpÒ¬-+!Ð4m>ƒB‘± 8Vºž^æ½®– ¡åјÚÿ©Úv.Àé,cyQ}Ÿ‰S'_.b ¸–ÏŸÝù0›ºwl„â|rVGEýÕ_ý÷Þ{/‹~1†ËoÞÂHáD}[aæ('ÓÜûÿð¹ûî»O»_sßšñÒ)þè›?àð؆&³Þf»äæ§Ìœ7ì×Z©ùù?LóŽ?Ía{Ký/úÓ¨õxO|±È›?‘ÃËÍÐfš8³÷;e®ý­ô¯í܈*†¾Ræ¦O¤f½6=lxñûWÿF†´Û­½zST /|{ŒûJ  {£Ç%ïî8}°l QÚq:Kع þ¾ 3|8 5:vpã–÷6N+þ…â¼±"æ±ÉyÝüù]?åG/ý _}î/8<¾ãû‹ ,ÓÑï’ïsÐÖü£ ^‹=éÜ@Ïa¥åõP°¸þä¼,ʨÿ”î.¿£@ûÂ'ö–X³}ö<3Y©(%• ëqi²:Jj8¾&»‰»w~‚[.úŽ5Ǽ;B!V½l,msó–rÆ÷ñØûøÚ®ÿÁÑç8õj‰áÃe }…~ÛÕÉØhê2'þqš‰£1Ú†›ntç\ßéÉ/NÓ·ÝáÀO+Ä‘áMËrìù€P¦ÍŽ;P™†Â:ÅEowësÞ~"äàÏlO±îêÙÿT ÏÎ ûìûé4—¿G†Ç¾xŠM×dØÿÈ$ޝØ|mŽÞ ’ Ñ¿#©ø(•,Í… v6Àî(cùÕY“ L„Œ-SšH¶­Ëoã®K?ÁM›ß‡c»Å3¥J#„çlP ¥4׬¿+úßÉS‡¿Ã·ö~†ý#O0r¤ÂèÑ Ù‡Î7mÕÞÌp¶ÜäѹÑ"(^øz‰ÃOlºÁµßÔ©˜?­°óŸlŸÅȫϥÄUN“мøý2Oÿs‘ëþK¥`Û;=òƒåIÃ3ÿR$Ý¥é¿ÔfzØðÌ¿¹òWÒtm²øÅ7K„ÕþÔ›,žº·ÈÖ[bRMT1x¤ÂM]x¶áʤáé{‹ì¸Ó£o›Íþ‡*<}o‰ë~'M0mxößJì¸Ó£çB‹—prOÈæêªÝ»î+ãå—Ü“bìPÌó_.±öªäk~)âå‡.}¯GªSñÒCvß_áÒ{–m¡- ˲Ð* ÁÉ)æ®ÚH%G!Î+¢MíÆS :Z+ŒI‚mÙ˜8&¶mâ8Æ\hç IDAT‰csr—÷Ç\?õ;3€Bc+­,v¼5ÃßfÏW4ƒ—¦ˆáW*\~wË<ñØ£/¤;Vat¿…“²pu†õ—UØû@…ç¾\¢k“ÅÁŸõNÅ5¯syò§¹à­Þ¢æÐéÝfc}žù—};l°åædåóÂZ‹ü€&,Ãàe“'brýÉ6Ç׬ݙ!q˜:â`Å).Ì’íôp|MßVŸ‘#qÅpá›óx)M׆ÙMgJí* IPQZѵÁm½¦ÈØx]nÏÚ Iw+Ò]š‰“ŽïŸN–E(ÇXÊå’ü;¹cð¸²÷:3kð<×óð\×q°Ûv°jUÝh¶”¾…âüvV—Tøä'?ɦM›5A´†š™«{GQDlb¢0"Š"Â0 B‚0 ¨T‚€JP©T¨TÊAÀxñOŽ~]“÷S1Sõcg:m:¥“¡&é#ã¨Ìkë$;çI=A´T;êÿ×l˜]™«bSÿìÅ#窾´·ù3o~<Êj쫜)¡SÎÎ婈‘#e¦†ƒú{SºÀå¹÷pE÷¤Ý|=ÌØŽÓÔÜdc[6¶cןk­±m»Ú¿Æ:mß :Bq~YQ›æ›FcªÕeTã¿Èuóˆ) ˪5KDض8C†8ŽÃµÞ/sy×¼0úžžø*Ñ1‚rÌÉWŠ *‘_ãÒÑïQŠÇ)3‰«Ò¸:™|î,þuÕÇ6^;a.µ~4~€•)¢ÜÆpúéÑ‘#ÁY«‡+ ÷üÿì½wœÅ™ÿÿî8asÒJZå¼²0A˜$Œ ’± †¯ ¶Ïöq`ÒÙ¯óclßö-M>|ÆØ$$rI"#”sÞUØ&t¨ß=©gfW»ÒJZIõ~½vg¦»¦º§§«æSO=õlýÛc^ܺµÁ¤£±‰ƒED4£S-ÄPÂ}ïh|˜èÒ¤ºŽ¢2‚;šwÆho°|ÎÄ5ÏqRÙeŒ,:ÃôâÍd:§V7é«›tÍgéZÐx'ÔU>)j$‰DÒ¯„ äÜ—ýƒ•9 ‘Œ}“´Þ¸®ƒª*h®Š£j8®†î8Ø™ÖÛÀ0l¬¸Žex«©FSZ4™úŽu|Øò76E—.VÔeϦÛ¢^4ãAТn 1Ú0•‚„£ñ>Bî)¤ý©ÃŒ££(F:bsg‹MóÎ-¶¯ððÐIL+»ŒšÂã¼i¦D”`#cÚÉš†'j4oºISýþ3ªêå~J¥IPoåšÈ?í”,#‘H$ ôCa“¤;“/¿”.B7®¨š‹æ¨¸šŽjÛ蚆˜¦²«¦,ËÂ6«¨âCI *ÇÞÈ>hšÇúè›a{Y¦·ÇhÚ§¸Ê ¬&ˆnºÄD1§S aª…¨ý÷’ö ÄP‚ÐÒÁí Í;cÄ2‚TF‡Nç¤ò¯3¨`lÚw&XÏȳºÉK… åD ÎÌ~ñ ¤–åûNU ‰D"‘dÑï…3¼º÷ÁÑNÆ.Š¢£ «º¸ªŠë¸žƒ±ã 9.Žc§"×Ꚏ­ëŽƒeV˲dŒaføÇ4v|ƒ[æ±6òŽˆ#\AË®8-»â”T `†4⢓¸ã9”B4ÅÜŸÛÇózslÁ¢(I‡`GкÛ[á”鬢3¶ðl¦•]Jeh˜/öLæ’mM×1¢F×µTÜ™¤/;´ÎH$‰¤·ô{a“I>‘£¨ ŠÈ#p(®7¥áª^€?WQSVÍqqôÄqÛ[]eè:–¡cYvj©ø}3Â×1-úÿø¸åï¬ìx[Dèh´èh´iT *Ö3ML¥ ÿ;«."3šv¶Í;c9Áº`BṜ\öuJƒƒ0LÏgÆ0\§à¤Cp*M:i¥ªj>A“í ,D"‘Hö—#JØd’=U•cÁAA¨Þ2qE$Žëz £:jÊyÕ±tÝF·u ÝÆN8[–‰eÅ1ŒÁœú!'Å.哿gYÞ¾¸h Úæ°ceFP¥|hÂr‡8Oùáô;GcÍF"3–Ú¸4íˆæ8j˜ã grbùW( Ty¾3‰t¾%Û š¸¦© z)ÿ™Þ )f$‰DÒ[ŽXa“dŸG!üËÓGS½eâšÐp5ÛÖRálÇA·í´'¹TÜ0˜¼Š£_aYË">k–ˆÛ €uÙµ®“½†BYM’&®j-ÄD&„” ½@èqD ¡§‚#mM;¢t6Û¾²Aµ˜ÏÍâ„òÙ„ÍR Ãðr9%ó7F"¥æ¥=ðùϨ©\^J"·WW˵¥ ‘H$I_qЅ͹çžKGG:Ó¶ªª¼óÎ;©×­­­œþù¾÷ÜyçL›6­WÇé^àˆ<GÁuEZè¨I? ÇqÑoUæ*ªÔRñ¸…®ëœ¼œJg³¼åy>i†gŽ%Ø»9BÃÖ(¥ƒ¼ˆÆªæ£8íè„0)@9„ºRQ„Ahvâ:y1{švĈu8¾²a­œŠ/fJÙEÍ Ãô9ë‰ÕM©eÛ©-’AôT-TO ‰D"‘Bú/ëƒ>Èþðî¹çN;í4ž|òIßþ¢¢"zè!®ºê*:;;y衇8þøã÷ûx9Ç烓)p%élœðýpU\UCU4ÍóÃñ­¤2 tËÂ0¼TÉ|T'¾ÆäÒ YÝú*·=E‹½Ó;×KÙд3Fq•Iùº©bщE':A ÐØGã] kDpÍ(‰œà.´î‰Ó´3†s}Å‹´jN(ù*SÊgbÁ” Éq ÎX¶“ÃIUS *“±g’±h’ß4‰D"9Xta3jÔ(~ûÛßò÷¿ÿ%K–ÐÐÐÀ!CRûEÁq–-[Æ+¯¼Âi§Ö'ÇݧÀQ„+RŽ“N°éºšæâ¸šíüK­¢²m ÝÀ²½|TIs|àB&•žÇš–Å|ÔúöfïdD:eC¨L§jX3¤bÅV¢h˜žÀÁÞ}Fò‡«Š‹ktâ‘”C°c Zêc4×ù‚ÊŒ¡L-¹„ÚÒs1tÓ4Êw¾óÎ;ï<^zé¥Ôv×u9õÔS™/¾ø"»víâøã窫®â /üÖ›ä{2šmrJ ¸€æÏG%„ÈÉGå8º·ŠJOZpI7¾8ºePkœÁ˜âSØÒþ µüñe©sŽw:ìXÑnªT .5p›˜ÚŠ¢¶£»a4'¼ïˆÆ€«ÆqõN\ÍŸò q{ŒŽF+§üÀÀDN*¹Œ1¥ÓRiò.ÙÞÏj™ÇÆÈÛü¼¨P1Ä[*®êi1 ¹A4;Œ*tl-‚£u"”tʃŽ&‹†­Q¬hV}ŠÊÈà)œ\~ƒ &x™¶3¬3iÿ*'o©¶?“¢(Þj§ #ó8I$‰¤?qÈãØüîw¿cÁ‚ÜtÓM8ŽÃgŸ}–S¦±±‘ûÏ>Û'j.¸àî¿ÿ~V­ZÅôéÓ0`<ðÿùŸÿ‰¢(Ü|óÍ=:—Þç£ÊL¸é UhN"á¦ãe·uÇv°uÛ¶Ñ +µT<·jÔ2°àßhˆlçæ§Xy —„ÕÅ…†­Q¶F) lPÝTpÔ(Ž%ÓeX¸Ðº7Nã¶(Ž•]cLèL¦U|¡‘]¦=H9kZ*íAf§Ì`zÙéüyœºÎÊ.‘H$ɡ⠛Q£FqõÕW3wî\®¹æÆŒ“SfÅŠ8ŽÃ|ÀÀsöWWWÓØØÀ?ýÓ?1gÎ~ûÛßòæ›o2gΜýŽûÊG•~-„Šª&¬7ªŠªºèzBÜ8.Žf§‚ýé¶–ÈGebIëaŒäüÐ L‹^ÎGÍO³ºóe‘ŽÜR£¥.FQ…AÙ fHŽ-hݧqG‘e Ñƒñ^ÚƒòPMFÚo鶦똩LÛzÊFÓôÄT[/S*J†ÎR|û$‰D"9–ÈÃcÇŽ ¤$ÿ Ø–-[øÉO~ÂÏþónë4hk׮忛oæá‡æ”SNá’K.áÁ¤´´´×çÖ•'[àäM¸é¸‰àt‰|Tš7M¥ÛzjUÒÉX7ŒÄRq ÓÊyák8%z95ÿƒ‹°ÜÎÔñÛ,Ú,BE:z@¡mo®C°®†8®ð‹œT~ Eª\ëLò8í;1eæ£w¡ÈÝ&‘H$Éa¢_¦T1b6lèQùÊÊJxஹ殿þzæÍ›ÇÔ©SùéOºßçÐ3“'á¦ÈH¸©ºét NÚÑØ0 o™xòÑt°- Ó49;ô}¦Å¿Î§ÍóYÖ:ŸˆÛ’:~¤Í†6ÿyÔB&—\ÄÉå_%l”&Rè¾NF"˜^RÔd¦=PTm?Sf^'‰D"‘HúýRØLš4 Ó4Y¸p!ííívYöø—]v555L™2…gžy†aÆñ§?ý逄M’ œnn !pÇK×X&î8º®cÙ¶g¹±ÓI7mËÂ0MÎ]É´ÊKYÖ¼ˆšçÑaïõWX+åIJ¯q|ùE„Íâ„óo"ÕAr ,áœö@U/R°ª¢*éÕM2Ó¶D"‘HŽtú¥°)++㦛nâöÛoç_þå_xðÁ ©ýóçϧ¢¢‚ÓN;‡zˆ#FðÕ¯~€`0H0L­ê+z7EÕU>ªtºÇÑ|ŽÆÉ©*Û4±m; 0ÈiáË9yàWYÞøË^D—ã*ÏcJÅ—h!T-)Z4´Œ©¦lÿ™îÓÈ4‰D"9:8¤Â¦¡¡¿üå/<ýôÓ¼ð 8‹.ºˆ‘#GúÊÞrË-lܸ‘Çœ·Þz‹3Ï<€¥K—bš&þóŸSeo¿ývöìÙCMM <òñx|ŸK½÷—ž œ¬|T”.Ò5莋­Û¸Žžˆjlã¸.®ãà¸.Žã DˆS×Ҳ¼˜¶¶6Î8áRß2lMU=Q“X¦“e[¦=H$É1Â!6ñxœ={ö0}út¦OŸxb'‰ä”-((àÉ'Ÿä{ßûo¼ñ[¶laÔ¨Q|ýë_gæÌ™©ۻ?ü¥K—¢ª*§Ÿ~:=ôPNÀ¿¾fß'3•@Õ=«MÊz㺉¤›.š¦yNÆŽƒ£^ú†”¨I¤|p]“XÌ £f¤2ðK~Gàì?)h$‰Dr4sÈô­øƒý ¼\Oþ`®ë¦3ƒþeüËùÂGž}öYZ[ZùÖ·¾ hªê­ÂÊ#`üQ‚e'‰D"‘ôK›#¿'L—îÒ5$WSù¢gˆ„ðâÑ:`À³¸¨JzZ)XÍpFÁ·_¦=H$ÉÑŽ6}LO¢gÆ…I U¨¸JRظ¡%Ó™½“Á¦aþ83Igå¤pñE –i$‰DrŒ …ÍA¢7ÑŒ3WRy۵Ĵ•ê ›DYMÓP5]7r§”2üf²÷L{ ‘H$’c)l=]Iå ‘²ê$Ÿ'Óx+ ¼œNøœ!{ŠI¦=H$ɱˆ6‡žoc"è_¦ó±’9å¤åMo7åÈ´‰D"9fÂæ0Ð?œÌçÙ«˜²s7e?ïêx‰D"‘íHasÉ'p2·ç{ž)lzR·D"‘H$ÇRØô²EH¶ÐÙßz$‰D"9Ö¦Ò]&í|K¶…Äb±ÔkUU1M3çý¶íå£ÚW9‰D"‘HŽTö=§!é÷¬^½š &PXXH(büøñ8Ž“Sî¶Ûn£¬¬ŒP(Daa!çwÞa8[‰D"‘HRØÔÖÖ²yófn½õV6oÞÌý×å”ûõ¯Í®]»ƒÌŸ?ŸÅ‹â3•H$‰äà"…ÍQD8æ„N   €[o½•U«Vå”)..Æ0 *++ÃJ$‰Drp‘Âæ(cäÈ‘üîw¿#‹qÕUWå’ÚMMMlÛ¶í œD"‘H$)lŽB®¾újÎ9çÞ{ï=~ÿûß÷ø}ýë_=z4S§N墋.¢²²’›nº‰h4zÏV"‘H$’¾C ›£EQxøá‡)**â—¿ü%«W¯Þç{~ýë_sÙe—qíµ×²qãF>ýôSæÏŸÏ£>Ê™gžé[M%‘H$IE ›£”#FðßÿýßD£Q¾óït;%µråJ~ýë_súé§sã7¦¶Ÿzê©üìg?ãý÷ßçŽ;î8§-‘H$É!…ÍQÌ~ðf̘Á’%KøÃþÐe¹'žx˲¸âŠ+rö%·ýÏÿüÏA;O‰D"‘Hú )lŽr~øaŠ‹‹ùÅ/~Áš5kò–Y¾|9à9gS]]M8fíÚµÄãñƒz®‰D"‘(RØå :”9s椦¤\×Í)ÓÙÙ @ È[G À¶m)l$‰DÒï‘Âæà»ßý.3gÎäÝwßeΜ99û;î8¶nÝš³¯½½¦¦&†NaaáA?W‰D"‘H)lŽ|ðAJKK¹å–[r–oÏœ9€ ä¼ï¹çžó•‘H$‰¤?#…ÍQ„mÛX–•w_MM sçÎ%æ”9ï¼óøÚ×¾ÆÓO?ÍË/¿œÚ¾wï^~õ«_QYYÉo~ó›ƒzî‰D"‘ôRØìÙ³‡Y³fñïÿþï<÷Üs̘1ƒ·Þz+§Ü•W^ɬY³òÖñØcñƒü€‹.ºˆ+®¸‚믿žO<‘p8Ì[o½EyyùÁþ‰D"‘0Š¢(é‹ÊæÎˈ#¸øâ‹û¢:IùË_hnnæŸÿùŸ}Û£Ñ(K–,ñm«­­¥ºº:§Ž¦¦&>ýôS¦NJQQQÎþ­[·òÑGÑÖÖÆ”)S˜4iš¦õí‘H$‰ä ¡î8Á`³Î:«GeËÊʺ-;lØ0† Ö7'&‘H$É!FNEI$‰D"9jÂF"‘H$ÉQƒ6‰D"‘Hޤ°‘H$‰DrÔ …D"‘H$’£)lú9óæÍË›£Éqž|òÉÃpF‰D"‘ô_¤°éçX–Å\à7ŽãpñÅÓÐÐpÏL"‘H$’þ‡6ýœóÏ?Ÿ·Þz‹3f`Û6®ë2{ölžþy¾ô¥/îÓ“H$‰¤_!ôõs***=z4K—.eÏž=¨ªÊ¦M›2d#FŒ8ܧ'‘H$I¿BZlŽfÍšE<gýúõ¬[·ŽÎÎÎG–H$‰äXB ›#€K/½”ŠŠ ,Ë"SZZÊ7¾ñÃ}Z‰D"‘ô;¤°9˜:u*†a¤^‡B!Î<óÌÃxF‰D"‘ôO¤°é§!RŠ¢0yòäÔ¾!C†`š¦¯ŒD"‘‹döƒ=ù“ýHaÓOØWãûæ7¿‰iš¨ªÊå—_Þíû%‰ähæ@ú;)tŽ~䪨ÃHw*{ß¿øEB¡®ë2kÖ,ß~EQº|oö>‰D"9éi™¯\f?(ûË£)lþ†É—ÉíÞ£”Ô¶ŠŠ B¡±XŒ‘#Gâºnª&Ë$_g6Îì}‰Dr$‘O¨øûÊîË&·{} @%§¯Löò¿<:Âæ’¯Ñ¹®¿¦L¤” a#„ओN¢¡¡Çqò4L¿˜QE ‰DrÄÒ• É¶Îd‹œ®,6IaJêu²¯B&¤°9t×@Å7>aƒ@¤¶ Úb,\ýG”“?¦¨Óáñ~Æ…µ×RªN4PP5GäH#‘HŽ4ö%h2Ÿ»®‹g…I¿osã2žY1‡U»ß¡(PÁÌ '²‰¹ IDAT?äÌQ—£«¯?T”,«ª*¸®ì/!D3PÒ•Í;—#FpñÅ÷EuG<=m þ?7ÕH;ëxnõݼ¶þ1bNÄÛƒ¡šœ9ê›Ì®½ŽÊ¡¨ªš%rrÅN¾Æ)¬D"éô¬¿L÷É?×uB°vÏRžY9—Oë^Ω§<4 kÄÙ£¿MÐûúÇì?ý呎65Ð «L²a !p\‡]m›ynÕݼ½åI,77³w&š¢sêð¯2«özjJÆ¥FªªdžîlrŸD"‘*ºó‰É¦ûI7eåBðYÝë<»r«÷¾»Ï㛜?îÌ÷] e©þ0ÕOª RàéHaÓ‡ôvÄá ×qSv{ój毺“¥Ûþ‹ã¯HâJÍTi©âfïVT¦þ³']ÏÈò)¨Š'n2.zÌ盓®K6Z‰Drðè‰C°7Í„oàçºnbøÑö癿ê65}’S—ª*” 6‰E\Ú¬œý!£˜óÆ|‡ Æý’P•×Gj*ª’mù–çHD ›> ×‚&Ñ8]ÇÅ.›>fþÊ;ù¨îyYu)P6(@iµ‰f&|g\èh´Ø³5ŠcåûsÕçðåI72®êTUõYmº8Iߺä¡e£•H$}H¾¡û4™¶c±dë?xnÕlo[“S¿f*T QP®£$¢´9– ©.Ns],§¼©…8kä7ùRíµT„'ÄMæ P œ#)l€žúÐøL¨‰×®ã°z÷ž]5—»ßÈ©GU*†(ª2IÌ*yuº¤¬i±Ù»%F<êäÔ1¾òTfO¼Ï :;%n2!!pPÈ´àx+’ÈF+‘H„nûÊD—“OÐ$ÿâv”·6ÿ•…kîfwÇ–œºŒ JÕˆ¡bdw•¬:Ù•¹Ž u—EÃŽÂõŸ®œ1â2.¬½–Å£¼¡¢¤DMO§ôe_Ù?¦— Õõæ†?Ùñ ÖÜÁº†÷rêÒu•Ê ÊŒ”€A€c œ¸‹ë‚¦+h¦‚ª¥Q´Ý¡akŒH›SçȲ)̪½‘©C.@Sµ´'c>99¿ŒöP³Ïè§JúAFJÝ?EI#÷u¹å}Þgt?øK¯fJúfNÍ»®KÌêäµ ³hí½4Eë³*‚P‘N到–j'ÂÇrq,Ïþ­ë 𩦄.´5X4l˵x«hLúe¾<ñ†”ŽOô•rJÿHC ›²ÿ‚ÆÅu×åým X°úN¶4–U‘7â02D0kÄáZÛvÁMoK¾GQA¨hFºÅ#.Ûc´7æÎ+)Ï…®ãÔ_A׌ ëMzd’4¿fÆzÈF6ا»û©«ý’¾%4Ú´µRÞçûOïV„¦“‚¦=ÚÌ«ëÿ‡6a“ì*•Œ¾W(`˜*fHEÕý了XŽ£qUx(_šp-ÓG\NÐ KŸÅ~À1+ltN¸¡s WßÛÿŒåD}õü#!HXgDêµw¬Ô d0è™Å†tÍ6>“kâ}Š fXà ¦­¨KãŽí –O8ªÉiÃ/cVíµ (–gécOlrßÁdñâÅ,\¸Ð·í‚ .ଳÎJ½~æ™gx÷ÝtØõ‘#GòÃþð Sò^r!–eaYq¶6¬áÑþ• M&K´sôœaÇ3{øÏ¨.A8"  1 3aÉÑ}N¢ÇÊQWVÂîH­®Ëê]K˜¿f.ËweÅìPPnP9ÌïoH²¯Lô‰9¾Ä“TÙ‹Mæ1’ÂFÉ:–f*˜¡ŒaÂâÝ´3F´Ýoñ. VsÁ¸æœ1W2 ¥Ïâaä˜Ëî} sÂõ-X°ænÞÝ2[øÌ‚…UòG°¼tóÖíÎ}}¨žïrA´Í&ÖîYp̆T©¢rX01¯lá:ËóƦÇykóŸ™6äbfO¼>ÛÁ½ !rV$·¿&¯ïÁj´UUUpÛm·á8¿üå/©®®ö•8p MMM<ðÀ|ùË_æŒ3Î8(çéÏ›ìèmÛ&óÊÚGyrùmÄ(ÉoÇ © u‚Åf0㈧ë5;îmsˆvØÄÚ„€­Ÿpÿê+™Q}S]Dm{¢4”¼wÒâæh§«¾2ó±»®+øtç«,Xsk÷.õÕ£ÅLʇøý SÀÔ ®«“ãô—vÌÅŽ¹hº‚öúÊ‚2‚2h»CÓÎMžÅ»9º‹¿,»«ïô|Ç}B³,í³(üé€Ty(ûÊccFØèœð–¦,Xuïo— S¤€ÂrƒŠìGbŸè®q&ÊôÙ@lr¢pÑv‡X»ƒÒxñp*†)« кۢ¹>Žwq„û۞bɶ§9qð̪½!ÛAÍ3@NÇ(mmm-?ÿùÏÙ¾};<ð–eQ[[ë+sÊ)§ðÌ3Ï0nÜ8ž|òI@ŸžC6É{))j>Ûù:úôæ„ ^ é ƒ&øMï’C„Fa…xËúuÄ:,·“…õ¿#D9ãLC¸. $œŒ•Ô’¦i9÷ýÑ‚¯¯Lt=4žÿ؇Û²`õln^æ«[UJ(Íò7ô*ßG)²”^Çt¶Ø¨m °ŠÖj ÆŠº4íŒÓÖà9·Ç›xzÅïX´æœ3úÛÌ5%¡Yé\Ÿ ‘§o9ê§¢ögNX“˜ÞÐð1Ï®œË§õ/ùæ„%1âÈZáäUÚ…¹“äH¤›©¨œ²=˜ŠÊ8†o**󲎑mrŒ B°PO}! ½Á¢¹.N¬Óov=nÀ˜=ñj«Oï]l2{¾n´»wïfìØ±X–Åš5k:thjßòåË9ñÄyá…8ûì³ûô¸™dvþIQÓÒÑÀ/^>—ÆÈN@PP®3pLoµGæ÷q8ɼÇÒ³žæÜ‡é½9M.»lƶ척÷i০/óܺ¬ÏÿZÓ½i#¤ êÞöÆíQwÄP€"­šËk´š’’RŠŠ‹‡ÂƒAtÝs*>Ú¦£ü}¥À³+cZü!.„ðVôÙŽÅ»[žæ¹<1»tS¥|°IÑÓço˜ú^2^§¾Ódßçú¿ç|ýe·SQ©2"w[êX‘Ñ7¥û愸È:/TA ¬ah¨ GcÇ´ÔÅiÙíw46´ _ùM.šp …CÐRSø2]ÃÁB»õÖ[ û¢²%K–PZZÊ„ ú¢º¢7‚&íì`;Žã°¢þ-ùàÇÌ[ñŸÔ·oHÕ¡é eƒ ¦°ø€O<‘ûî»[n¹…Õ«WSYYé+¿téR.¿ürŠŠŠ¸îºëX¶l>ú(S¦LáÕW_Å0¼)‹›o¾™;3_þò—L˜0… rß}÷ÑÒÒBqqqâڥﳤµ¦±u7=<Žë‚aSŠ0Cªïèä¹ÇRùF¹ù^gßwùÊvw¬Œ¶ÐûcwUŸÈ­?c³"ŠË Q‹;q,Á–OÛpl†ÎåP]QCyE9e¥¥0+¥Žt«Mª¿Ì˜rJ ‘ñ:_ˆ‹H¼×6>Î kÿHSÔ¿ª/\¢S>$;ͺ¯þ2Ç:#r¾çƒb±IÞ·Yç—º>™e3ûðŒÂFP%X¨¡ÒŽÆíÞ”~¦£±ŠÆ´!³˜5ñz†•MÜït Gò}w°9j„Íþ Û¶Ryœ¶¶®ðÕ,Ð(« PP–åŽÔA“|Ìm”‡IØ$¡d—MK$µïXªê-Å j©øvÜ¥¹>NkÂÑ8Éࢱ©Ø½ÉG¥(éãõEƒ7o—^z)guO<ñµµµÌ™3‡«®ºÊW®½½ &0jÔ(/^œÚþÛßþ–Ÿþô§Ì;—믿žöövJJJ¸üòËù¿ÿû?:::(,,Ì+lÇÁ¶m¢Ñ(o}…;–^ Ê  {eû‹¥&I7?þG³° ë%´ÜÒ½¨‡æº8{¶D8Ëü7&TŸBeE%ååå— …0MÓ³Þh*Êèíi¡É0=4í±&^^û/nxȳKQ¼ˆêå5ùý 3òœPú±_›Ôùæ6™ÇÈ~n(‹¼a’H›CóÎÍéÐ ÇšÁì‰70¦ò$ÿ *)pˆ#Þy¸Ç‚&Ïœ°eÇygó<®½›ºŒé&€‚Ròš€…ô¼ögº9ÇdÛVHS\Çsœ‹´ØžÀ)ÒÑM•ÊaAÊk´ì¶h©aÇ;ÛÖñÀû?â+þ‹™®IÅvBEQDVl‡Ä1…È8ÉNwÿì%—\ÂôéÓyýõ×9÷Üs9þøãsD Àœ9sرcsæÌñmŸ5k?ýéOyõÕWSÂ`Ù²e8Žƒ¦iðÉ'ŸPXX迆ÂÙql65}šÚW˜ÈGÂ}Ò²|§ö±¹ß «ZjG1š§ÛüîØ:†G>G4%cY¦i"„ h©¹#åÇ%[и~3¹ (\_ÙÝËkà•±ÛRuªšBɃÒAùý 3òœPÀ¾fç—Üù©í¸KÛ^M· é 5BE¡ñaâÏâݶ×BÁÇu/òqÝ‹L¬:ƒÙodbõ)K Þ4ir­JW‹2¤“q.G¬°éNÐdÎY&—kg6Ô˜áfÑÚ{ièÜžªCQ¡¨Â¤¬ÆÄì爣7Ê^?ûX=l°¾m¢­‘V‡@X#T¢¡›*eƒLJš´ïµhªïtÙݹ•ÿýèßxfå˜9îjÎóínc;xõg® Hü÷·ÁΙ3‡“O>™•+WòÔSOå-“ŒksÇwpÏ=÷¤¶'ñ–-[o©ø…^Èüùó9餓¸çž{8í´Ó˜2eН>EQ²‚8ºlmIgszM®ç_{ïù\ Œ cµåM9š!-Õ^šœ-D£Qbñ8ñxÜó/ɰb)¤Ïն于ٵ·c‹ÖÜÃâÍOw"©:uS¥t Iqµ‘r M"÷iÖ õå§;ô ¿L“8¶ £Ñ¢³Ù&X¤yVœÊ€Q!*†oÏÑxåž·XùÆ[Œ)?‰Yµ×sBÍù©~ÒU\TMM œìéO)pr9â„M—‚&£aveB팵òê†ÿå…u÷ÓÛ“ªCÓJ˜” 2}_ÄÚk­´ Öáë°Ñ*á3¬QTePTeÐÙlÓT'ÒjÓÝÅËnõÅv(0Ksb;xc¶À!õ:õùzÑh§NJYYŒ7.o™Õ«W …x衇òÖmšfêùã?Î/ùKî¹çN?ýt¾ò•¯pûí·§êÎ<Ïä=踭Ñ=©+™J¸w¤ÜP}L²à„RTEϘRPp-Õ˜•xÔ%J ±x ËŠ{)0Û7ÝŸ—|ç_@AÂÚ”Ü/Òýdv§–,X}7ïlý›/fW ¬Q:Ȥ°"¿¿aÖÓ¬8àuxɧ`²vg㺂Îf›Î‹`N¨Ä[yZ14@Ù`“Ö=-õq¬˜ËúƘóö·V2‰‹&\Ç)Ãf£iº÷ý(nî”~"á&H“É!lº_á´ï9áÖh/¯{˜—Ö?L‡ÕœªÃ$FŒ”ïˆWyÞ§Y'Ð'mߪ_‚6ØìÝVÔ¥%G3<(Ô—ê„Kubž¿B{£E[¼‘§Vü–…kîåÜÑWrÁ„¦$èÅvPMh)1s(ƒWUVV²iÓ&*++sœŠ³)))aîܹ|ÿûßç'?ù ÿûßYµj+W®Ì{>©0d ³îŽp¤÷úGA½S å¹â^ƒKå r]ÛËçå8®ãà:.éÓÿèéŠÐäŸã:é{ÕuÙÚ´Šù«îȉÙ.Ö)lzÕ}•ûòœP|¨}pÈó~ô—B@¤Í&Òfc†5Â%:FÐû *©6éh´hª‹ëpØÚ²‚{—þ§Wü–‹&\Çé#.ÅÐÍŒ)ý„À™SøþÔ û; <è×!4»ZçwögJ¶mo ¢eY4tÔñÄ'·òãç¦ñU¿O‰š`¡ÆÀ1!†O)¤d ™5}U´Mäi*Пí^mS÷™µï‚‡˜öÝ.Ûß·2¢÷u]¶«ÝvÜ¥uOœ†­Q:šm\W(ШN\çÒ&ª¦±ÛX°ænnZp2}x3{Z·&–Ú8Ž•åÚcÈwYSŽûˤI“xíµ×º-WWWÇ'Ÿ|’zÏÂ… ¹âŠ+X½z5¯¼òJNy¿Â¢¤1µ!½¨Ge…ØË›} J3ó¥fºj¹ýez™¶ãØØ¶åõ›–ź]0çÍ+ùùKç°tû?pqQ(ª4:¹Áµa¿¨I9Òvq\ˆ4ºDš\"Í.ÂÉWbm‚5/DóïL°ö…±öCw;\6¿Ýƒ¾º‡STÙÄ:švÄhJ$#N:^=®€šÚ0¥Þu®oßÈCÜÀOž›Æ k¤3ÚîûKOŠõ—Ç ýRØô¨Šô°ãz+P’u-›øßÊNcÑÚ?u<ÇÏ‚RšÚ†L*ð¢ŽföKÂÿôÃÇ:Ùôf<ã8àþ«a½ÍîÕö¾ öÆËŸî¾Cè ‘&ÁîÕY½N/NòRº¶ ½Ábïæ(m{¼ùc= R9<Èðã ©@7âN„—Ö?ÄŸ;…—ÜÀަu‰ïÏòu¾ùN_6ÚýèG¨ªÊ/~ñ sö¿ýöÛ¼þúë\sÍ5¾}É ÙŒ3W_$­Š’üêq¤¦„Ò^½Ççs’ôGýãÇ¢»öЕ I:µ{íÍaùÎÅüîËøÕk_âãºTM¡t Éðã ©"öçqêVÐ$vØqÁ«··óæ¼}W‹~ÞÊ+ÿÞÆÊQâéwÆ;[—v/"6.ŽauœkÝ´Åá³§b¾mM‚ôpÚƒ&ÞU+ê9ïÝ%Òb#„Šu3ìs…'‹ììàñoæ_žÄ³+î¤#Ú’ú½sÜ #z6 <ÚéWSQ=3¡vÇ©®e= VßÅ»[ŸÂž€HŽ8J¼Ø!¾Êó>í²L#Þ)hßí°¶j¼Få8-%Ù›²ŽÕÕ•HÌ+w´Ø„ uÂez*PéÀm Íu1â‹Å›ÿÌ[›Ÿä䡳™=ñz†•ÖæÍGµ¯eŽ™þ;wîdãÆD"ž³ã«¯¾Êرc>|¸ï='žx"?ÿùÏùÕ¯~Å 'œÀÕW_͘1cX·nO?ý4UUU©äš+V¬`Á‚œsÎ9¬\¹’¹sçrê©§òùÏ>÷² ‘ŒLrxQB³¼×˳}"ÁÍ ´réé”S¾®ëòÉŽ—™¿úÖ7¾Ÿz¿nª”T›”Tg¥<8€¾òä«Â” ×.´Ö9¬Zåí{Ú9óÆB4C¡¨ZeÆ/zfA;ØQAû®>ˆ½Ðƒ[¢«"ŽåÒº;N[ƒE¸Ä›ÊO:— Ò²+îýE÷ð×Ï~Ís«ïbÆØïqþ¸ïS(÷úGõðMé÷7ú°é²‘âŸöYkyœ67.gþª;ø`ÇsˆÄœ°¦+ðVëd'YËó4ëÀ¹›6½g×J‹öÝ.b…ñ0>÷ò¹6¬})ÊÞu6ÑVA¸\eò%! +ÓA›6¼cÇGU#N3)‘^^Ú¼Õaý+±ÄqTFŸmR567—Pg£Ë†WcÄZï=A˜8+€Õ)ØúžÅÀÉ[Þ¶P …²a*ª©0ì#õþ¦-ÛÞ³™|I€¦ÍuËljgy޲+ŸS5^£aCóv‡ŠQ5'ëK”ÔgÜðªEÓF#¬0ìtƒmïZŒi.É]îiµ‰´Ú˜!‚r@Fq•Aq¥AG‹Ms]ŒH«ÃÒmç½mÿàøAç3»öÆTMÍÉG•)pºj°Š¢pçw²dɦM›Ào~óFÍÃ?œs-o½õVN?ýtî¿ÿ~xàB¡&Là–[naöìÙLœ8‘Ù³gsÛm·qõÕW3pà@.¾øbnºé¦T¿lŽ•ÑÑ‘€ªè™(ŠÖ;M’ýý)J¿øN»‰w'h¼˜]ϱ`õ¾˜]fX£l IaåÁsVT(©Ñ8éÊ0¯ÿw;Ç{n€Ž—epêÕÞ²{×5 £Ô¯´0 UÆgv[ogƒËšçc´Ö»GPŠÓ´¡8â› IDATÙá„o† +´nwÑ2®ò®fÁ‰ßS¿Êâã?wrÎÿW ´Õ»¼÷p'ã/0éË:{78|ò—Ÿÿ§0…ýÖ¦P‰Ê°Ï›l{/ÎäK‚B…½6{×9¸¶`Ì “`±Jç^—åÿˆ1ä$ƒd(„íïÛ„+¼ëcE–Ÿ»]Z¶9 ?Õ`È4ÍoYl^l1á"¯ƒÙ´Ø¢u»ÃØ/š¨lzâu§‹k J—m:Öéë´ÑM•‚rƒP±NA©÷ëpiª‹ÑÞhñqÝ |\÷“LOÄv8=%p ½Ü1)x’Û¼£{?<·ß~{Wßr^f̘ÁŒ3ºÜ?eÊ{ì±^Õ)é=à TSt TEëuÝ‚\gÛÃMú¼––y^~1ã¤ü/,;Î;[æñÜê»|1»Â‰áÒüÁYO»,Óô BÕX–í^'êÄÍÛÒêšç£ìZmsÜÅ!TCaÍ¢H·~jBÀ“ Šk4b­‚‹c|6/Ê´òav6 _¤àõµVDP2Laø©›ßŽ3ù«A`)DÛâÁŽlÆœgo¬Z£h Ja•Šë@ëN—oX =Å[å´fQœ@¡Â€Úô ¶§'ßgêh¶éh²é”˜¡´£q{£—Ã/ÚÑε÷òòú‡™>âÿqáÄQU04VCSQ3Jcð ::M÷æ ´~™Íè³ ÊF¨CO1X‘5GÝÝ%µb.mõJk F±V'P 2pL; ©>NÛn‹»ßdÅî7[~2³'ÝÀñƒg idXmDÊÜ ¹YĶÆÚgoJ  JCËol:8(ÞH׊ Âe‡Æ½OWM 2”þéNØküý¥’%ºü+BÇ!žˆÙõÜš{R1» Ë J™ ò ÍzÚe™ý%X¢ÐZ—t¸í½8f©ç-s5=À‡ÿ×Ùe]•**®ëÝ_ƒ×ùèñvÌËËÖŠ f‚ª)KßgsmsžA¸ÜKR÷™Fã‡Âªô½4ôdŠÑÞ5P«Ñ¸ÑI ›$] œÌðÝi³q;LŠŠ Њãha‹¢ ƒ¢ ƒÎ›æú8Í1^Ùø(¯oú§»„Ù¯cpÉX¯µëÁ`ê²þò° ›Lò‰šLá¶¿À³+ç°¡ñ£Ô{Â¥:eƒLBÅoÄQ=Ñ`÷*›×~×Nñ`aÓ jN0Qó\½áŸ7ùìïQ^úu£<‘RU«'#´P62üKQ T¦ÒÙèR2D£c¯Kû—×ÿ«Ý»á…—hM×»¯)P¨®P}£œš v|ä ›ŸÚ”U •)]^ƒ’¡éÆ*U°:ÁŠ œ8×h©[2¤ë|&WC P(GqT¬&»%ˆVC/Ž£ j¸ѸuWœæ]qÖ5¾Ïïßü&ÃKŽãËÇÝÈ)Ãf¡ª:(**.™>ðGSãL²ó‹-K-ÆŸ |d×¢:ÑVÁžÕ6C§¥ŒpáÍ;:8ãºJjzW_oسÆ&P¤P4(}ŒíÅÙ»Ááä+ÃÝ¿93ÇFØQAýJ›ASÒ–ÈLL-DXO8 ~CKŸ“;EŸÎ{±:xiÍ#<¿î>Z¢»/Bpq•á%iì"iÖÓ.Ë(±vA 8÷K³:vÌX%è=ïšx§`Ù_#´ïq «hô1Âåûß OÔ$•)t6ú/Bf?,UhÚÜ…)öÛ'dÒŠq£àD ÃÁ(Ž¡Æ=œx§KS}Œö½omù ooý+Ójf1{Ò Œ¬ø\:ØŸëv)nŽ&«°éʬ›)jlÛbÉÖ¿óÌò¹lk]d8 `†~Uu˜rYˆÚYAv­°ØðzœŽ½.fæ&E•©Lûn˜H“KÝgŸþ­“ã¾dÐdï‡Eéfàh„jN4˜0Ók™i‹³ØWŸu¾¹7ë ):_ÓÙàR÷‰Í賺ª+Ù¶Ûš© ¨irS¾³aߎwÉAKP 'V¥dŒ\»%€Õ@/ˆ£—ÄÑL‡²š¥ƒ´íÓ\gKËrî|û{ ^6†/Oº‘ÓG]Š¡™¨ªÿâdûàél\'Òì²åÝ8å#»¶²å£c¯Ëòg¢>as¨Øünœò‘šOØTO0(Òs1ÕÝÔT¬]°zQ”êI:ªé¿aÃF1­ ÑnŽU“Ù_戚Äô|{¬‰E«àÅuÐo¼ %Õ&%LßuYÕ ׆½ëmOɽ'µ ¢ÍnÊ2iî¾Ùöž…PøÂ A@Û.‡wÖÙ©óÖL|+ª„À·*«+ºì:2¶wן祇×Ò3ð(J1Õ°ÿwÍÒˆï ASÐ Å0Ã*Õ£BT ÒR£e·ÅÒíϰtû3œ0x÷¯ŒpŠOÜäó(é/¡Yl²…-;ÆâOòÌŠ¹Ô·ysª–h Õf:‚kª‚¼O»,Óœ¸gÒ4à CO6‰¶úç„ó• •©Œ:Ó¤i³C[½Ë Éû>NåXõ¯Æs¶‰L>'jžß¥‚J•Hsl™x¢©z¢ÎŠgâ8qAeÇçž (P6\e× ‡Òajbj*q-öq!£˜^ˆ]L; °ÛLì6-d£—ÄÐÂ6ÅLŠ«L:š=Gãmëùã’k˜÷ÙíÌšxçŒý6#tÈe]]UUUèúÁo>­;Zë¦~+ÌGÿÁŠŒPúsîYkS4P#XœÞ¶{µMéP Í„ÆM6BêWx«‹©„JÒZã&¯þêZ=ål™D8^]í .áR…µiËH¼Cмݡr”Îîµ6VD0p’ŽðΣµÎ!Þ!èØã²{•¢BÕxEÅïû†7JoØ`i„Š*Æzõ-ÛZw:¸.Uk)G{! q³÷™ö®·Ñtot]TiR`”kQغ©+&¨îõý•;w2`À€ýºŸ’¢¦¹s7Ï­¼›×=BÔöÂ[˜!•ÒAŠ* ÿða4àM­x6ŠkÁ¨/²ö T J‡hÔ¯°©ë]]+º‘ѾÛñYVv|ì_¦]>BcÍ¢ôTùÞu6ñÎô‡ W¨D[Ý®û¥ý¤y«‹nBAuÂ7ŒKºk«**…f9ºjvYV8 Vc«ÉD/²0Jbè¦KŰ e5Zw{ŽÆï|‰w¾Ä¤êé\|Ü™Rs–O$-b&“Ã&lºò­‰Ù^Yó¿<»b.{;w‰œ$ƒºwÎó²;zÆÚ—c4np(¢bE`Ï:›©Wä9øx!…U*‘&—ÆÍ“..Ì[6›a§˜4muxõöv*{Ëv‡áŸ7rrîWUT­.Wyû®NBe 㾘ÝQø©™jðþ# û¼ÚËÅ!™Œ>Ïdùßb,½/ŠªAU­Š‚¢åŸÚRB³ M ,°¯c;:N§Žbº¥QôB‹‚2‚2h{"sã6yÿ'<½ü¿™5ñ:¾8ỄLoÙèÁ´ÚèºÎôéÓ1 ƒo|ãÌš5‹ššš>=F’mX ˜ S=Q'Tª°ó‹á§¦¤?{*Jí…}.­|?þs„¯QR£y¦qá9­覑6^‹ÓZçPR£±zaŒqçõOl¼÷p'Ñ—ʱ:[ÞqXÿZœSF5=ÿˆOžˆP>RÃŽƒªÁªQ¦~Û[ÚÛVçï´ïqaµ…¦+Tש_ùÿ³÷æQ’\å™÷ïÆ½‘[e­]]Õû¦VwK­¥[­ !dI€„$,0£aŒ±Çƒ7øÆß±1¶‡sÆg@ãe0Œ=ã> c,À˜EH€„P íkKÝ꽫k¯Ê-–ûýq#"#³²º«÷êV>:­ªŠŒÌˆÌŒûÆ»<ïóz ¥¨±ßUBX‚þ³c¯…L ‡¬~“Ëð6Ÿ—ï«R\,ñ«ší?©Ñ»ZqÎ; ÉsäUãP¿ä#,ÁàÙ:-`ÿ e¶þÓ0Å…™Éó÷²îª.V]zêZŠ¥W\q®ërûí·óö·¿}Æõ4[¶æàô.¾ñÔÜ÷òÿ¦M«lQÑ5è$"oõ'òÏ9‹B¿ÅOÿ¦D®ÛbÍaº°æŠ×~ê‘ëµX½ðiüÛrÉ;ÝL9®U9¿¡ÀwðÇ]d¡†ÝUÅrº:¦† Ñø™ý?æ™ý?款‹¸ùüßgóÒ·%Äâ3͹Zë1 ó°{ÎwÞy'+V¬à¦›n:ì¾Í uº6ο=÷%¾ùÌ_1V1sœæ2“䛎iNì °³&óú0¶Ógú iÁîY)gfŒ"xå±×Êc!……’î%’èú¤4˜k”hßí´°ó†S ‰Ý&Ë#,èúë5ÑtC„Ž6T'4Ó#! -tÓ#!‹­ûVÇ5þU‰K~=“7µ67±ò˜¦cÐ8&{BÜN“5N:ÐŒí2¯),ó@i8ÀÎ ¦jžúj•Ë>šEªúy¡A© Õ…2) 4Ÿ—ˆöo¨D?cR´P!v—)S‰¨ôäUB3HnÈC‡š·‡6ü×oø :2=Éwr"íÐЛ6mbÿþýôöö’Ëå¸öÚkyï{ß›èÚT*e&&'ùÜC¿ÌöIà [}qa tËìzãúðý?Ÿä¼›³ œ«xéUö?ãsÅGóÉ>÷}fj†cóo4ɦ÷gY°V1üJÀ#_.ñ¶?¯ßÔußþO n´Ùô¾,ã@m°Æ?šGÏ|£Âèö€7üVÞ|A!üø/§8G±æ—ƒ/ù<úw%6ܘaÙ%æFðä=eœœ‘BxôïJô¬”¬º²îp¿ò£jݱÑðà_OQ\"9çY">8aÄ?ÓN®7€é¡‡þÇ4—}8O~ÅôPÈO¿0ÍÕ¿ßM1ß²ljåüÕÖÿRË7›ìàðŽ ÿŸ\ó‘E¸²ák¾æÂÞ!,;äµ'§¨–:*+¸Tþ& XØ¿¾ èîê¦PÈã8.RÊDÞþX044Ä\ÀÐн½½äóyÞüæ7óÞ÷¾—7¼á HY/ßi­Ù3þ2_ò¿rÿ¶ÿ ôò=6݇!bÓqwhtXϪ !È™.Ë´C§Ô4ã»C˾“ÊXÈð¶·(èY¡ÙйØBÚ¢±DwmZ3ô’O®Ë¢¸ÔbôÕ€®%ûv!|%@šž’É!ÙNʉä"¨LhJÃ!¦ë©4R\\ïp*i´¯ÉõZžfbwØÀ±©NijSšB¿±›SûC¤dºëÇÐi»½nú^˜±;ÈÊ4MÏÑÉî³ÛËÔõ,s>ª«ŠÊ׳]¥qŸ±½5JãfÛòîs¹ùüßã +oÆJ})g‚“3/JQSÕQ>yïUì™x;c±`E¦ÅL’Ö÷±H‹‹ê_´¥ g¥¢g塞ahgMdšÞŸJ3¡Œ®CzŸøØÅE²áâ?Ü¢ÀIìÎìL±=¯¬yé5Ε Ë?†¨lý³íLµ+bêÈ]ËêÏ©Œk*£š|¿`bfçÏ<úÖF6©×ÍÙEÜ9H×þ«Òhߢ:”¡6âbwš¨$¾Nz–¸Œï«1²{„»ÿs¾ýìøÌ?dqךÖž»`Á¶nÝÊå—_ζmÛÐZóÅ/~‘»ï¾ÇqX¹r%7ßün®¸âG}Œ}Q–¥½¹ž–l²yñ{U&÷ã{¬XtA]}»w•äÉ»üŠFfC/øô¬”xÎOŒe¾×j,Á î˜ÈxÇÏjÌåqa¯»!ӸĿ Ëdt†^ò©MithøÓCò ŒV‡6E§ÕÈÆv×ð*¦‹qßó¥äÜ…%ß[£¿ãÈ8J' ,àñÇç²Ë.ãÕW_EkͶmÛøêW¿Šëº¬ZµŠ÷¿ÿýÜpà ŒZÛø“{Ah®B¯MïRû„àCl:!%'0ß_ïªÙn1õƒJGÌ Åg:-o²’›zìôÌ'/ gG›GüzÉò˜î¥(à*.²fØ]·CàêÏë\"ìG¶KD÷ sOèZf5ÃÉ œ|ô^èŸyŒÙ` IÁîFZ‡ÎºÃŒOËGƒ’ŸVXn€Ý]Ã¥€áUvŒ>Ígᅢ¼ø¿ùÔÛ¾9‡³<}Ðòª[·nGüb»ví"“ÉpçwÑó6¾a%{V¾œüݳÄmšI2»„ëŒ-'h‘ÎŽÃð8 sòåfy°<ªyäËeº–Z¬½þØÓ¬:€y”ÇB¤-èZf±òÊú NYyÕ‰vcTq AmØ¥6æ]TBæ ¿¢g‰Q3ö*!Õƒüõ—þ‚Ÿ~ó…ãwàYÐÝÝm4t"c6:j›û÷ïçg?ûZk–×ɺÛ5Ù#ìÐØù¨‡rÏ|£>6C9‚ÖØpãLâú‘"Û]?Ÿ˜ã×@fLôYщs 24H(Çòذ+‚êÜ¿ìZ4û§cak2ñîÇ=^øn…Å8t.‘8YÁÁ—|OUE¤í ž†ç˜¹;‚Û¢Ï,º­Ïb7+îÿáý<øàCضR*ÊÔÿ4~ww7;vì ŒùØ»w/=ôZk6\º”¥7×L{2п2ÓT¢?Éëùf/[áDˆ͇cEȨ=ÄžçK396GˆÉA[]‡xþ°j]™'k§¢ŽÙÞ>º·+TˆÓå¡:«I‡G+ŽÍõë}ÇæD ͱY¸pá!96l?ò×ßõs\5#3ãW4ßûÓ)ö?ç3p®2„ܧ=–l6iø—ï«6\~‹ÍwøÕ€ÞXCiXv©ÍsߪҿNÑw–‘‰¯Œ‡T'µÑ0šº—KFw˜òVÚ9‹áv˜²ík×ÈvYt-‘ž¦<¡É÷BçôPˆ¥,/Ë+–‘–ˆíå{™ÉÁW* nÈ!t-vétxþ‡ãl¸¶‹BŸ¹i ½jnþ‡l;Uâ /dhhˆ… 6pl.¿üò¤cJkÍ"½ˆe]òÏO~–^¹‹‰!ɃÞLŽMÊ^ž|ó2×#¡­< „ENqUôaFtåGa86µCslºÎáÝÇfæ9Š$;“Ïçg<Þ ögvt'{0/0"D7žóQÞ|Ö¯ò×ÿo>û— —ö°[™áU3&¡?êŠ:éα:5ñöy°`ç |Öòi´>{GfȨ"ªUÙé8¼å$â(xÉGwEM˜2IW¦ŸÖÿo>ûƒäÝbÃl©¡¡!ÞñŽw°råJþðÿ0銊 TxÞ'·‚6dÞ%›ff1TÆ´íï|´ÆÀ¹Š%›lÆwšN:!àì·ºì¶nÌ,g_çòì½&ölx{†—¾¹l‹ƒW†_ÜUA"F¹þúÌœ›Á6{C~ô¹)®ýgò­Îyg–§¿Qæ‘/Oã~κ֥ؗã¬K{xêë%~òÙ2è2«/+’ïm4Yë¯îdÛO'øù=9ëŠ"ë®îbËm}<ñ/#üðì%Sx•B¯â²_éçäÏ?<8À;ÞñÖ¬YÃýÑÍèŠjn´,®æ×.ûKn:÷÷¸÷™¿äWÿ/Sæ†=²Š®E©®(‘Ê$/t"ßѱ85éíóÀ^G®*Q…£„-@Lð'3&ë§5IWTµdÊ›kz/â¦s‡ÍË®CYê¸dçZvE]uÕU'µ•˜ ü€ ¨TK<¸ã«|û…ÿÎÒvбŽM瀃rš47Z•>Ž+Ž~±ÖÏmfGÚtF9Ó5vZ2çSÏÓ©÷­KkÙеB&$Ii"õ¼tʾUçUó¾ŽÌ’‘…†4ê¬çÕ| šömxqóÃèØT¹ºFÎô˜ÏØž*å)³­7·˜ë×þ&oZý>²N)eÂ}8Üð·cÁl:6͎ͱvE jS•±P¶ÄB"„]‰)!DŒì>hBB´h(L¦¿‚ø÷J£!–¸¢î«¦Ÿ¦›ž?ÛßѶV¥-¿ª &ùÎ,ÙlK¨ä‰•ñ'g%<’¯Ÿ~ùÔc~MSžðÉ,£´;˱ÓŸŠ®¨Ã騤›¸LLÂß÷ÚÇ¿¾øyØþ¨x‘ŽMÎÈdtô: :6óÇV {Ùðèv-ý7vÅïK§K­lÙ [u¨cÍ´»c¯äz-ìlã1Zž—6ï1# ¸*Ÿ84‡³íõ}šÞcò9¥6XU¨¡:«X¶ÙÚèØì­á{æšY×w7®ÿ8ç-ºªÁV6ÛË£µ•Gë7œÌ‹ŒMòAêèwJÚ\¾ô66÷¿“GvßË÷_ý{§^btO•±½U:úºœœŒ^#z‰¶`[G۪ѳB¦º¨tÓþ‡Çóß©2°Q±lË!Ôa2ûzš_|¥Â>–Ã-þœF^ Øö€Çô\¯ÅàŠÅÖ#tGfqeJÄ©ñ#<¡C½A}:QÄj&zŒí­R+›'öçWò–U¿ÎeËÞMÆÍbK{†#s"188xRŽs(XB"…JØXÝ*Ò½82hBB  ÌÐDí$¿ ÂÆ.ºã´¾iÙ(\¤å :D£³!S”Gulå ½M­zó‹-šó¾i¢ºæó:2}¼kÝ'¸zÙà‡¯þ/~´ó(•Æ9°­ÌÈÎ*uåḬâœÃhs4@ÓÚVÎﬕ¹zîÞk¯sèYi1¼-à•<¦‡Br}‹.P,º ¾'YddGˆÃŒãi/…Ô¨bÕQCH³“_ ßWc|00OßÐwo;ë79»ÿblÇtB6ÚÊÙ¾ƒÓ§Ô±‰ióÄQ‰€„H©ØÚ·6Ë)K£ UT±ŠH‚'ö×ÛW%ðÌÂÌŸÍÕKÿ›¿ ×q±•ƒe‰(bgÚÚœ%l+ƒ“Ñ8XHa6 Ùñ$»68:¡t€Ö¡ùGHØê)–‰P…iͶ„Bb#„¢î¬4g€Ú˜#„ê+p˜wºxóÊ_ç²÷òயòÐÞÿËTí ïUÝ]¥Øï$³¢ÞâI³—³äøüdý+ãšUWÙY|)à…o×XxV‘ޮޕE´pÔçŒYì™e¨hVTìŸÔJ£{kL¬¡µqª6ö]Ë5+>ÄŠž8¶c23Ñ@êžÛH>pÊgE5G éYRJÂ0@†%%ú®duÇ%¼8ü3Üÿ¿y­ô8¥1ŸÒ˜o¦{/r“éÞ œô]íy£2®Ùó„Ïä¾l·H&sï{Úg裲`­dàœ¹ÏêñÊšÕ˜ÉvY,»ÄÁIÑ&÷†ìzÌC‡°ðIuBãv º—ÏŒØuh$É—¿ÁF9‚ám~%Ä)XìÆ'Û-¢Ï¹6­¹äCY,K.ÌðãÏ–°&»è\MŒÚKL2¼£Ê¥èCÚGyá7Ýx¬L´@3QÄ¡azÔDå ?yÒÊÜ%\¶àv–tœƒë¸ ¢P¶J ¤µ" RÆõtƒÀ±r¸Vþ¸•š^ox=ø:Bˆ}žvp,)‘Z£µŠì\=.€ úÞÆúâ5¼0öŽ~•aïJã>¥q''ét(ô9$UÝ&{)#[©,)ì#Ë z~‚Õ[zX}‘I?¯ˆ©äéÏÿk•¾³›Þ—AÒ}éûU ýÿj.ñÈâ}^ýIL§àüÛŒ½\t¾âg[f×Ï=V½Ñá•jt.‘œs“‹Ö0x¾âÁ¿*±ïIÅ›m¶?è1x¾âì·:hý~ñ•2y»‡N7Ÿ/üp‚±=5.ý•Ôàóiü[# 5TG¡"Âx ™8Pcl_ ¿j¶9VŽs‹×sqï­tå4ÚË(4öÒdò’k¥)c“ö qrN™õœA‚‹G:ZQRšEjÛõç-f!Ò’,‘ëY˜ûO •wðóÑ{xiú~ßgtwšhìâd㺲À\«ë8Ó#>~U3°¶€&Í»äl—'ý Öd lBÂÐÇ×>¡¢q“ûBzWɆ®ìÞÕ’é!s!O ô®©Ÿ³s–×7| Å‚•ŠŽlÊrHºú0µ:Ô÷ÝöÓI¶ÜÚk$Æ4E)—Ù²XAØQÄÂäÁc{kÔÊArngå®äâÞÛX˜_ݨp麉ScÛ*ql¤%“ô»ˆøhV|{;}©ceÉÈŽSRnJpÆzÇþÆæ“ÁËP¦l/-@#5Ñ\ºú{Õ€°b¾¢Y3çô^ÍÚŽ7²mò~>úUö{ÏQ+‰´F瀙†g¼…°p­<¶(Sаòâ[¿>Âî§J,9/ÇòM…HWH"°PÂŵL€è¨[LS´âÚPÞ·‹Õï(àXŠ ñ µ †&÷…,¹¨u©bOHèÃÓÿT‰ºD5~ÅÌ„SÖWŽà™®¢Ãz¦ª4ª±„¤<$X}y»€´Š«á{R¤gwÁ+OqÉûúpòÖ‘Ù˦SÈ™¯bªIy¾‘l¶¹²Èùogsï»(¸Ý*Àqv[ÙØ¶”*ÊØ4– Dê>z¦aÞ……Æ© ÑÚJ†ºDÒZsp¤RH%Q5Å€ZÍ[3¿ÃÅå÷òØØ×x¡ô‚°ÆÄó/ßmÒ®Ù¢ÂÓe¼ Œ-28âD¥ü[T ‰eI$.h°­29•£Ë)UCTE@Vvh­C¤ð¦F ‚ZãkÑx!,3­UXQ ÍB+²|Šör®CFŽ£3¾Q†DÝ2½Ã@£ÈtáM¶Á¡Ñõ*‰ Áãûªøž9 6kóWsQÏ-ôe—⤧­¶íg&’²OœY—µ·DãbMŸÌ©^´‡;¾.YYÄó¿{§g¬ÔúA3"]’²°"²·Ù¦”ML4–@Fk$ÎrJ%QR±V]ÎÊÂ^›üMÜÍîêøµ°‘h<è ¨„“T™Æ9+Tøò‹ ¬Ï±óñ)¶?:ÅSßåºO.ÁÍ[F!u=y‘z¶@ #j€-³äT>Õ^hÛòq¬ ®t ] úiìNÇ€dñ1wPÃfÓ oJh=K®w0á•BnQä: Žƒ²vãZ9léF—ûÌ‹>ðLš;S8–ÿ$þé# U¬\]`³V2z]“! gusAçMœßs#9§Ƕ±ÛN—êë RöÌÖnK`Ú5iÌ‹®¨øwˆy6Ƴ$$(L MXq·”©'{–qll¥¨Éʶñ¼¶ZƵٰ¥r¿ûÏ–þ /,1=ê3=ê“)Hº] =6<]A WPâÈfðd:$µR}0`¾Gag,ö¿P¦py Øÿbåò݇ÿ¸»3<ûÝq‘O:SÆ^-³xcE»‹e.¶Uèt@ÀÔAol/»“.§˜"št¬j,1•ÙæK ®þèBœìŸ“²óB†Xù 2W…8â¨F Áê‡ÖÞÌE=ï¡ËÀvTÓâTf.OìФœ™Ö ó¯^Üê\â<’%YY<âëí´Ä)t‚Žüгݾš^wž9Ìq‰A)eæL‰˜S/=Ä奶òPž‡çÕX­¶°¬ãöL=ÇÖ‰{Ø^y„0ÐDãE.nªzŠj0-²¸¢pÄ%S7o±æŠ"k®(ò¯ŸÙÍþË,»0O÷R—±Ý5–o*°ç™RÃóº—:ìyºÄò ë*¸6½Kr|>`Û:#›Y/E .wÙÿB™•· 6èØ`vcᲦø,½qAK±b¿ÍÁWª ®7¢5#¯ÕmX 僽t|d¡‚•©-O½®é±:I¸Cösaç»ÙØý62v6qfl¥p.¶MvFE¿´‰Ã–Õª+Šäï3ó*cÓüÁ&ÎMÄž¦I«F‹5IæÆö<<[áÙ~äà,âM™qQõ=<1öMžšþÕp‚Ê”!Û‹®—b¿M`Õ(é,ŽUÀYæRÊX¸6ÃË?™¤:Rì·Y¶)Ï9oíâ‰{G™òB°í§“œûÖn¤mq8æùŠ-^þÉ$÷ýÕ^–m.°ë‰iOsÖEÖ\Qd×Ó%¾ÿ¹½ô­rÚV¡¸Ð®·¥?Ï9~îÍð«!ßÿÜ>Þø¡~:¦›+¦¶¨«PÁÊÔ#ŽêtÀØÞ*SÃ^b4‘çÜŽëÙÔsE·/q^‡&Z éìŒRé h¨§£èVÆþT!ÝF™Q…d{ài,W¼~œšÓ Bc)sSŒçŽIMlN:oZjOîõÕ*k£^¾ÐO&Å¿±¬¤$¡|O™,¨ïÙÔl¯Vc¹:E…u •_å±±{ØVy­ƒF¢ñ"3 ØÓ%<]B GPâð„ÙÿçúÏÊëRŒí®Q‡ap]–Gî:ˆµðÊšÑÕ†çžw}7÷~??úâ~–œŸÇ+› î+/)°á-]üð¯÷ñÀç÷³ìÂ<š³ÞXdíUEö<Åàú­•‚ÞÔà\¢#µÍñÜB}Á ½Ë]¤3KÊ2vhY,aK†C# 4æsàÕ2ïU¨•Ì "#‹lî¼…· ~‚³º.§í$“Í’q]ÜLÆü‹ˆo¶cÛ¶#èû5¥HI¬]“.E5_;§ FÖÇóÏö £<œë´q²VD\?†î‰6Ž?”¹r4ƒÌÜXûªp×S,)ä äòyrÙŽc'&q¶ðd`öëÜäc‡Z49c1¿Â²„i ·¢h^™5¥¢àв$y»›ÕùËX¹/¨1⿆&Ä«†Lô˜ñ±¤ÀÉJ4ž.ãS‰8|³ßØ-)ß[ctgBŸâ¼ºÉFƒŽ;Øô¯ÎpðÕ™Éy7ô mAÿê –dŠ’%ä©N…Œí®!,X´>‹[8Y‹e›òÔJ!c{Œ-X—#[”H%X~QÀÓŒï«Q (.tX—A9vÆbù¦<µrÈø^Z)¤{±Kÿš R òÝ’žå.̈”õ×v’-Jº;8©iñ‚¾™ÖIu\cåjXÓXyS¢Íø/—˜8P‹4» ×^Í›ú>Ì5¿ÉâŽõä2YÜL†Œë›éfp܈(l×¥Kö1eãpŽÍñ¸~Öo88e#šÑœÉˆ…ûÒ¿ÇíÞa&ÿ‚Àˆ‡¹‘ø‘Oþyžïáy&‹ãû>•Z™§Ç¾ÃÿÌdx þaXÐÑç4LÊl‘ñ uQ±#~s4iz¤ÒÝéí-FÄ;¦eÂK£Ù.Ihv=1Í/þy”k?6`Z6S¥¨tJµñX)raóy¥÷M~w"áãˆL ‘« ìzÄ1u°ÆèÞZCi.'{¹°ø.ÎﺞŒ›o,7E­ˆéZp=;9«¢)š2Ø0{ùéT86ñç†!¾ïQ©T™šžâáWîåžûÅ…ý+³X( rŽî< ­è™?uÓãºißÃêØÌåõRÓ]/­Îéº8ͯ—<®[ž_zß°0Š•7CU÷o3¥õïgM÷% ô° ¿Ÿ¾¾>:‹dsYÛI²‡'ózk•õMÛËØNj­ uH„ÉÈš0HÙÊh|ï{ÆVF£@¼Z Ïì¥ç3V9ÀÖ±{x¾ô|]IŽ©œHZc¡ÊÃt¬¶ÈѸåwÝh/9R!ÞÞâZ™aËšŽÑòºÐºñ”ZÙËÏk¼nµ¥Ù V®’”çO3¶¯ÊÄþ_?ɅζtßÊšÎKLÉp¶ìLCyÞziæP2'*lThViÖÙ88–%‚zŠ5 %–˜èÃPJáû>à+;Šœ}ϦæyضÍEö»ØØõ6ž›ø!Oüãþ.tHŠhlÓ½Ø%SÔô4µ¨®lˆÆsÛ;xàóûP®Eµ¢ͦw÷§æx#MP€Ðˆl+[YoAß_c|_ ¿VoÇ/Ê6uÝ̹ÝoÅU“BU¶ÑVÒ85IdÑTžáÐXuŽ-iê¾m"Ïöñj5lÛæûjÎ*^Á«?ç±É{Ø_{€ÊTÀÞ¦±3Ý‹\:8ԉƮUÀž ÑødãGq¬O‰#ŽLá6¶ Žîi$,pÖ²¥ëÖt^Ž­¢º¯SwhäS¨1@kÆÍã´ˆ:t#7Ì’¶­¸qåÿÃúýxa‰ê¤ÏÈž*=‹\jz ?¬àˆ<)§¿ss@ <´[B¸#9º·=첪üv²=Y2™ ŽeU¬à:?È—3²7ÂÁÑ¡M§TÖíe "[¨@(•ô•mãø>^D2ö=Ûv¸Ü½ »ÞÉSãÿʓߠ£5LÕ˜Œføõ,É)HC4J(‘Á=aÒ§pj&›m>¦F=ÆöT©LÕËóBHÎʾ‘‹znaanu½ ÔI•Ts»vÝV¶âÌÄ™™t†¦þ3*åÍ“ðd`Þ961šÛ¿ÓÛb¤¥¡ÁÔ–»,Âд?aq8|<ßG6¶òðmSžò=åÙœm_Ϊâ^›z’­w³«ú ÀtIx¥ÌðÎ .]HŸr8F…I+#rÄcéOkØžÉÎ8õˆ£4n"ŽÒ¸ß°ë {[º~™UÅͨÙ;•¡I¤½còb³C#­$3;8q÷ɼwhRHsÍÙ,ìXÎ/õ˜ïîûïU(ù ®Í™²”?Åg=GˆY~?M!ÂÐŽ2ãûë×û’ÉkéÎÏçÈård\CnW2.G5ÊÒŸjÌÅÁ±,aܵ@ÿ‰¯SÃYŒJúÒB!JJü¨}\&]T¶ïáÕLIÿ’Ì-œ×yÏM|Ÿ_L~‰`@4Ão 7/éYâ’ï²ñ©à늙&â€ð4‡¥Á‰²Ù2 ÕLðÛWMºë ®Ùµ¥ûz³KºA¸4oÛQ6»¹D/“f™¹;41Zß?ÏdÌ[Ç&FúËhæÛÄ‹6ý¥ÆQH}Á¦E8Ê⡹¹qqÒ Ö÷|j^ÕöE,ë8=SÏGÚ!§Ù XÙô,Î í€j8AÉc#7ç O6ܸe„J‚‡MÄQM‚–e6sQ×­,ë8/qhZ¶lÛu•àx‘¶*7Ͷ@cRbŒÓeqÆïII…ãØd²Y.Yt3»¦ŸåÙÉC`ô*^Ý:IG¯M®(Ét¨úÔå6N(üjHe: 209â%õ &7³Ôº„ŽBù||.O6g:¡lÛž9Æca6{Y·•©LN(À¢EI_FüÃYŒƒÂ˜³¨|Û65ÏC)Å&÷œÛu/LÜÏÖ‰¯1ê¿ɇ½/˜~Ý‹\Šýe=jx8Vþˆ‰Æó‚‰fipËàVV]€t|o•ñ&B°ÖçßÌE½uÍ.ÇvŒÝlѲÝÚ¡©w‚Εo=bÞ;6inÑ6ÿ(sZa²¨ehX’ ”¨ $ˆ28¾R(ß&°}ìDÛA±ÂŽ´JÛylü¶U~‚Ö:4íƒãûjzlz—e°3Ö¼#Bƒ[A»°Œó’Ì$ÙÛHFX¬Ê\Ê–žÛÌŸÔ„ãÅ™”¢,…© GmøV½õ°± u¦Cs:/ÐøÚ‹ß‡”Ûvp]—\>ÏË?Áâ½çó£‘ÿAUOšèn¨ÆÄЩ>ó6”Ÿgùäõ,RçÑÙÙIgg'Åb'ù|ž\6‹ëº©RTëY;ó ÍYœÙÂæ’~Üqj…VK΢ò%¾›iáx¾á,ntÞÊÙW±mâ§løCÞK€q$‡" ˆ®A—®A“ñ®óÑxžg¼­©€SIÁ^%dtÏLB°#òl,^Ï…]7QÌô*DÒžÝXž34õl¶Õ`/cµý¹”ççûuy2pZ96iÌ\´ „Õ°ˆÍ ´¶’›M¨uÒJM üùJ™ˆ$Êà(Ï‹¸8>ªf³È^Kþ÷)ßÎcc_ãÅò µ©¥NxLxd:}Ë㺲!+áâ07«“ ¢Ý Ú-' 4ð"µÑý„`!ge¯dKÏ­ôçV4Õ„£iÛ駨mûHkÂgÒM—¢l[‘q]ü\Ž 8?|3Î:~rð±ÇÛJ…ÉS}º¯kØA®ÊY¬ðÞL1ÛC¡£ƒÎbìØ) d³YÇM®å§Ã5zø,Ž5cÿØÁÑZÏà,šõm¸‹u΢o{Ô¼ª¦Ø`_ÅšâålŸzœ­c_e¯÷ `‚¦‘]FwWèèwè]’AÚ!U=I5˜Â‰Âc#Ÿ€œŽôÑ™2ØuáÀÊTÀèî*Ó£^î«ÈùÅwrA÷äœÎú §ÈœÑåÔ0"F$Ã}jtìØð!m/go¶y=ã´ulbÔœøïfGFÙ›!–Öu²\"¥E˜šr }³hE( ;°£Q žçcÛ+xKî·¹¤üË<6ö5ž/}?Ñv¨Lúìzz ;cÑ·"K¾SáSŧХm\‘GqŠëÊV€vKh»ÚqŒì®2•šIfšïºü5\Ô} =¹Å(¥fÖ„g85V"\özth 1kcº¾l'$†IWŸ×9gjj’ƒS{8P}‰Qo~X%ˆn(ÐZ³¤c‡Â&С““¦ó)SÌËæÈç tvA¾ŽŽòù|’­‰36§ë5Ú*‹s('Þ¯ç L™*𣦠Ó&®â¬·RI™Jy6kíKXÙ±‰]SÏð󱯲«ö8±fÌÄþûk䣌·“±¨é5]BaˆÆ§–h¬í¡[U'OyŒîn$ä­^.ì|7çu¿Œ“¯kÐØNJ³+Å9Liv%£2jÙž³C3?K¢§§½cc¦ƒÓ\¢’ÄÃ.ŽeZÅ,Ú¾o²­”Õàù)-œÅ\“ýl©ÜÊããÿ³Óß¡NÆQØûü4Òô.ËÒÑg ²C0‰C›œÄ´«–¦D«:A²20²ËXÓ°E–s:Þʦî›étû“9N³ÉxÏ•äözK¡Æï;L躑¸¡iIÛ&ãfÈåò,(/¡V«Q«Õ Ù=-"}ÛÉ9z4ß Ò¯8¢v]×…3Y …<ù|!ùÛIuDµ&hž^HÛÆøï¹pcÞbìäÄœE¥|‚ H²Ýmcû~$öç±R]Ȓ¹ì~‘­ã÷ðjåaˆ†TNxLx¸É‚Y2Y'ãàGq2¥54Ú®¢Ý2ÚŠø†!L¬1º§‘ PTƒl*ÞÌÆî·à8™& šz—SºDßR³KZ©ñ‚´Ó2Ó^¶šCáŒqlb*"‰öÀjÖvˆ•Œ#m‡¸¶,•!Ì©¨¶l+“vµ}#øçy5¤àÊ̹¨v3OŽ}‹'§î¥š—ÀÓØVbèUAÏb—ÎKT™¤ÆŠ'VÀJ«*¡SFËÆˆcdWãLÇê༎ØÔ}y·»1;Ó”¡‰5=bþŒ”²Ìx‹6ÉmfÖÆ®/µxníØd³Y²åµZ5‘ôñ¿îØ´™‚´c£”á‰9®c$ë³Y²Ù¬é„ʸ '"}]Ÿ h¶•ñ¶Ù8‹­›2êœÅ GRU,øçÕ¥5–Ûç0˜?‹ƒ¥×Ø:q/•ŒÖÆy¨N&ãíZô­4ï@Ô(SÃBa“Çfn3ü’÷Ã!ŠRÍ Mh— 2ˆ”é¾c{« „`€{9›;ßÃú®«£¬¬dgìVÙì¸yb61Í.Î<[y"pÆ96ñ¼Yú IDAT1‘@“¶CiNjSSN‹WŋՂú‚M¥]mÛæ2÷v.è~§×0õÏLÀiÿÞYaxW…Î…NÔIÓx¤]mÇâx£j—ÑVÜ‚kŒî®6t€de7tÜÄùÝדuŠ3kÂéù#‡© 7Ϩy½94i4DÀ!(¥RÜ Û±ñ2²¹\ä(iû0" |Iº·›ã @¤TZ•Tfˆ®R8Ѽ2'æEdø8™ÌH;ƒ®Û£wp¢¦Œ(ÛXq©Êp}Ï'P6¶)¿§8‹ƒöÞšÿ].®ÜÎÖѯñBù>m²É^5Êx+Aïò }¡ð©2NMLb“CéüÑlh-B§D¨ê„`¿Vçê°qý-pÎbK÷m¬)^–hv¥›'’ p–¡”éŠÙæ7½íåñÄëØÀÜlc•ÑzH+Ä’a˜”kÜ&ßô£yTim‡‹Ý÷p^çõ<;ù~1ñu&‚½æàš¤“*ßkÓ·ÔtRù”ñ­2R»Øä°ô܈ÆÍ‡!¡]&P3#ŽÑ½U¦ˆ£ paÑÔ„]';˧ôè«eM¸‘äÖ^ i´Jï›jä,Û6n&“Œ BãÔ.ŽùSRNm ¢ù>¢ë6Ž˜eÔ~›*¤ˆï1ßáL.©ƒSoÊ-“íVJ!2ŒN`Êú³s—qmö#\\þe¶ŽçJßÅ Íœ®ÀרVfèÕ2=K2t.t°dHMLQÓ(Å>ã­-Ÿ@•U•Ø’ÖÊ!£»*L{3ö_äžËE]ïeUçæ(km7t…Î:ÇI6Ê\Ä÷™ÓV„ô4ÀíØÄ˜Û‚­E"^¥SâUVhZF¼JÅó¨ÒT³j;Üȹoá…‰ñøÄ×ñw$ÇŸö˜öÈt˜º²›—¢J ªfòs˜Gê¹µì(â qì©0~ ± K-fS×{ØÐy-®ã&¢z QÇ\æ8¥j³“ÜZ¯'¤›¤;/4³RŠ0Lv ¢L uŽ œZ}£3©kϤûë×lš ;›Æ’y‰3ÿú=²€°Þ”Š€0´Â\Û2nÊPsá,òKÙ_ã¢Ê{øÅؽ~7ûkÏ%Çjšý/—°,A÷b—j)dj¸Ö|긲Èy…ØÔs9§Ëh5u8Å­þ­æ8‹fWú³kãÄ íØ´À¡"éÙx³i;„Rãû&UéÑ09ßÇ·ʳql'Êâ,r΢?ÿ»\Vy??»‡ç¦¿OFQ††‰3¨…ÍúŽ«ÙÒ{Ýîâ†y$‡J¡6ÏqJ/ÌY#ŽvM¸6Úh‰ã,œE¯Þyêùkí7°ºx ;KOðó±»ÙY~<9~i‡5#'»¹°ëÝœß}U¨ëu%vS¡dc—S¢CivÉÄikvÍW´›Ã`®uåzm¹QÛ¡>*@!¾ô#âœÄVÑÐM “s<ÏÃq–ò–üGyCíWxlôk<9ñ-jA©á¼l‘áœÎ븸ç6:³ ¢Tiª³)iEL‚R¨MÝMsM¡¶kÂm´ÑF œpÎbª¤ïGT~à³ÚÝŠŽÍì-?Ï#ÃÿÈ«¥ŸÎµ,Ú ¹¨ç6v]‡£Ì؃ºV—jä*ÛH1´5»N[´›9âȈs)m‡ó¨‚ ll}´m#Ðø¾Gàx¾O&“ášüärÿýlþgžÿ.ZkÖw^ÍE½7Sp{[¼@æ‘H©ª‡špÓçÐFm´Ñ 'Œ³eãû†X~`2;+³°¼s#+;xøàWØ5ý$ÙÉæÞwsN÷µØÊŒÅ°£lŒ”-õÚš]gÚŽÍbnNJÛa–yT¦õ10­a€/aà6ïã„a"­Ÿ ³\Sø5®á×ÌëCéҲLÛv‹î¦¤&,e{ìAm´qRp(ÎbŒ#â,±­ °mešH­;ð}ÂèùÙÜ:–tš?ù“?áù}½½Q#„LJI±­4e¦ÔÏ´3ÓÖì:mÑvlŽsrpf™GUo}4¡Q*4ÓqÀ0‚0q†´ŽæÅÇDjÑ%†ÕèÌXé’S;…ÚFmœ´â,67eĿ׳ޱÍ4¶Ï’!V`¡CiÊTa€ eld<ŠÄŒ# Ñ€WóÈd2är9“1OÔ¥ëcZéuµ5»N´›cÄ‘µ>êh{sÚÕè;H)SÒúMNÖ‰V[’ µÄŒÈ¢9ÚÒB´S¨m´ÑÆ<À‘”ôÝ´’q qæ;Éx7ÙÊôŒ5­AJI6“!“Í&™êö²Ef¦=öàôGÛ±9N8ü‚mÝú˜î u}q6:54D1@CZ´™Ô6#âh§PÛh£y„#*éÇœEK7d¼ÍO3[- ê#Ibñ=˲p—Œ›Aƒs“…é²|ó8¶ÄÅi‹¶csœÑzÁ6µ>F‹OkÝØ€•lu}¤f5'7b¦#ÓN¡¶ÑFóGæàÔ3Þñ@ÙPG6S†ÑH’0ÉlK)qÇq@j@ÚNg§Qâ"m?¡-qqº¢íØœ 4.ئÖGM¢íï›è<³0-,Ц[€k+švÓ¢¼Ô°@Û)Ô6Úhã4À‘uš}Ã04Á U/?ÅÙñØîÅ ëèÈ6ÆNÊn6ÉÏö؃ÓmÇæ£aÁBF»yÑ&bVÒJž—.E5:7332Í?[Sm´ÑÆ|ÂáœVŽŽÙßjpjâÇc}¯ØI9"{Ù{pÚ¢íØœ$̶`ãŸÍ‹6ÞWk¯ÓúO=óù­n;…ÚFmœŽ˜­¤_7‚Ѷ( ^wpbãñÓe&«1s}¨àïPÛÛ˜ÿh;6'éÅr('gÆþé5–òWf87G›qÌ6Úh£Ó-KúͶR4‡æASr²"Œæ×k;3g6ÚŽÍ)D«¨$½}ÖE6‡µ×^ m´ÑÆÉÂ+¯¼Â‹/¾Ø°mݺu¬X±"ùû™gžaçÎÉß½½½lÙ²eN¯¸€°ù÷øïXþ¢ñš“Ùm{y†¡íØÌ̶håuÚh£6Nžþyþâ/þ‚ûï¿€o¼‘~ô£ ŽÍ£>Êç>÷9ž|òIV®\ɇ>ô¡9;6i³½lsgÎx´›y†öbk£6N7\ýõ\ýõ\yå•üøÇ?æ–[náÚk¯mØçŽ;îÀó<>ùÉOòðÃÓßßÌÇ=œ½œ­#´3Öáwi£6Úh£ÃãsŸûBþðÿr¹ÜðØøÄ'>Áù/ÿå¸85m´1ÚŽMm´ÑFÇ›7oæø;wîäÎ;ïlxìw~çw8÷Üsùà?Øò¹/¾ø"ÿøÿÈ]wÝÅ+¯¼2ë1^~ùeþþïÿžû‘‘ãzþmœh;6m´ÑFm7üçÿüŸÉçó|æ3ŸáÀ|ï{ßãî»ïæ _øÂŒÒÐÐÐ7Ýtçwßüæ7ù³?û3Ö®]Ëÿñ7ìw÷Ýw³víZ>þñóä“Oò±}ŒÞÞ^*•ÊI{omœh;6m´ÑFm7,Z´ˆO|âLNNò©O}ŠJ¥ÂoüÆoðð¬_¿¾aß0 ¹îºëxþùçÙ¾};ÿðÿÀÓO?ͯþê¯òéOšï|ç;T«Uî¸ãÖ­[ǽ÷ÞËg?ûY¾÷½ïзׯi€¶cÓFm´ÑÆqÅïýÞï±téRþöoÿ–ÿïÿ}»iÆ—¾ô%¶nÝÊG>ò’í·Þz+_ùÊWعs'¥R©¡ jÁ‚ÜyçØ¶}‚ßM§Ú]Qm´ÑFmWd³Y>ó™Ïðþ÷¿Ÿ»îº‹ïÿûd2™û}ûÛߌΧ>õ©d{L<Þ¶mkÖ¬aãÆ|ûÛßæcûú§JGG¿ýÛ¿}âßL§Ú›6Úh£6Ž;n¿ývr¹–eqÍ5×´Üç™gž!ŸÏ³víZ“«V­âóŸÿ<ÿøÇ“}¿üå/³qãFþÛûo¬Y³†¿ù›¿Áó¼“õvÚ8ÐÎØ´ÑFm´qÜ!„@JyØ}J¥wÜq®ërß-[¶ðØcñÅ/~‘?þã?æ·þöî;L®ò<øÿ÷ÔéÛW«ŽX (V@HÓä‚!SŒóÚ¸^6$Žc;vâülÀ‰“—7Ø LìPíP…AÓ‘)T@½k%mßésÎy~œ©ÚUC«ÝÕý¹´—fçœ9óÌÎ<÷Üçiç«_å‰'žà‘GÌb‹Q@Zl„B ‹Y³f¡”âí·ßÞç~žç‘Ëå0 ƒ/}éK¬]»–SN9…Ç{ŒU«V QiEµÄF!İ( þá8àöžžî¾ûn>ùÉOﯩ©áÚk¯E)ÅÖ­[AEU‘ÄF!Ä Ëd28Ž@2™pŸ+¯¼’³Ï>›G}”‹/¾˜×^{ööv^~ùe®¿þz-ZTÜwùòåÅD'›Íòàƒ2qâDæÍ›wø_Œ¨*’Ø!„T—_~9õõõ¤R)<Ï£©©‰ .¸`À}—,Y 7ÜÀóÏ?ϼyóhnnæŸøº®óàƒþÔnÏóhnnæ´ÓN£¥¥…T*Åã?N,Ê—&ª€ B1¨î¹çžÞ×¶m~üãóãÿ˜Í›7‡ijjªØgÑ¢ElÙ²…¾¾>vìØÁ„ ˆD"ƒ]l1JHb#„bD˜·Çb1i¡û%]QB!„5$±B!Ĩ!‰B!F Il„B1jHb#„BˆQC!„BŒ’Ø!„bÔÄF!DÕó†ñ‘™LŠÇŒÆÓcØ–…mÛX–m[†Â4ýcùÙˆTV!ß¾âåŽî ¬Úù"»Þfsï v'7Ve¼œr–Æ âåç~8ÜE9`¶f\>^NŒÍfFÃ<¡ÊxiÙ¦ÄË}‰My%u=×ñ+i"ç¡•ÿÊ“ëþ×+ «žŠ9dÜ8}Ùݬé~€ˆÕÀG'}“ÙÍg  …B8N@ÀF)¿²†!TˆÃd x™ËåH¦ãüîÝ›Xºáþ¸Dïá+è!šz!8F§:Æ …x¹‹ÕÝ/µšùؤo1«ÙŸé  8A‚Á€ÄË}¨úĦߙG¾’®Þù¿|ãz¶÷½_ØÃÒ„ ¬î7û‰ÃF¡È¥=2 7çÿý¹Nî[ï¶ŸÃ…“®§¾¦…H8‡çEŠ'ƒåƒæä,DˆÁ3P¼Ìf³¼¿s¿|ãzÚâë {ùx1°‚/7…"—òÈ$Kñ2žÛÍ=ë¿ÁqçsÞįÑXÛB$ìàyÁ`åø‰—%UŸØ@ià[¡9uÍÎ7¹ù…KȺi ´~|€ºñ SÞôáàd]ÛÒôḭ̀ª÷iv­ÞÀÕ­ÿ×m¨§i`š–_A‹ÝÏò¾ 1Êãe.—cí®7ùÉ‹‹É¹ ñ²aB€Úq/‡‹“UtnMӻ˗+zþÀîÔ®:úÿâäêð<¿E­_¼Ì;ÒãeU'6å#ú IM2ç—o^WLjL[güÌv¨ìê#¤uuÀîjµÇͽ”uÀmøØÊ}÷1ð Ž×ïñ œ †­aÚÍG‡ˆ6Ú´­Mâf=:rëyvë휭¾ˆR é:š^˜`gȉ¢‡nÀx™Nð‹7®+&5f@gÂÌV° âåžñiûöùØ?Ââå˜Ö±¦R¼Ü•]Í·ýŠ…êsù‰ùÙRºV/Eu'6@qŠbáìãáUÿζÞ÷…nhL>!Š®æý32*i¾ Å·*û¯ì>µ×}Õ>÷í÷û@Ï¥*7TT´ýO•ºâxj/å3¤nÐ ÕLšeÓÛ}(WñNò&í>…c´“ý‘ÿ†ešÅÿ² •ƒ§/=?^þnùÍìè[M!^5'†¦öeôÄ˃‰o=×ÇK+¬ªÏÇËã"lz'ŽroÅÇ¤Ž“™ªÏA×uLËÄ0â,)™þí«Úô®ü ¯Ø´÷mcÉÚŸïwl¸”ÔxŒŒJúUûÇ4—Rôn÷ð²º62%„”ry¥çWÄã}Ä R©™LÇqŠÓPôJ*Ä¡*o­)Œ­ÙÕ»…¥ëo+î3af¤”ÔTy¼ì§ÊBH6©èÝ‘—æ)A?^âðr×/‰Çã$ Ò©Ùl¶8Uè·Bñ‘¨j›‚ò銫v½„ç9€"Ò`Šù RÅþbXõ(z_=(ˆ5[„jLÐá­§+ÞN2™ •N“Ífók¹(Ô_I…,…ÄÆq?^æ3˜X“E jøûH¼‚Z¬/kšíb¼lw×ÒÓ×E*™$•N‘ÍŸ’‰—UžØ”¯–é8;ÿ\ÜVÛÈï4L… Í$hFÁ1Q‰Ñ …G[|5Éd’ôž‰§*Î6…Ly¼t]— o·Õ4Ûù}†«tƒ«Êhú1t«,^ˆ6ø'êžæ°3±–dª/s¹œ/óïñ‘ÞjS•‰Í@Ë€»®Ã¦Þå2;’?û®BîE×F—Lß¡—ê@+îŽwslx1{ÈÏw¨"v=…RkY¿¢ógˆ»³ëI§ÓdÒr¹\EW”âƒÛ{¼|—B„´Âùº(ÕmЈXu¥ßrùx) ‰Ý•]K:&ÎÍ'5åñòH›U™Øº)üŠê±;¹©¸Í´´ÂNû<ÂPÿüéλÞÏâq\|—GïöA^¡ê`N‡4™1 Í*Ý—³@A \Jlú¼d2r¹ÒÙGáz5Gz%b0”ÇKÇqÙ•ÜàoÐÀ< iÝC/秺 /;\úºîõÚòñ2W¼TPñ²/«;±ÑÐ*ÖdðÇ×à€Ûg=­Î|5²A3Vy§ÒòÓ»)Vt•?ýÔqÊþbOÎ@„ åñÒS^ñ2 º®úx9¬]Sñä~¼ŒVÞéé @7´R¼týË_¸{ÄKá«êéÞ…JªògõÕ]õÀÍ*V=–a×*ÏU4m2ëãAQ¿f´¯uXóT†ønÝ€ú£LŽ] T[ÊO·¾žeý Yœ¬¢åX 38|UÚÐ-¿IõÞÏS~u\\¯Ô¬ªFÌœS!ª[y¼ô\ùÜ—lBádáú¡;÷7u›ˆ}`ñR)¯xé oxYh™;’g“Vm‹Mù¼Rþ]íÞý]šÎõÇ]dî•aÒ}oü:Yü k:̸ ș߈rò_…QJñöý©âã;Ö9¬z<äSlæ]ÁÀæeÃ3¾F× ¢V~º¢ð<·¢¯Ø?Qq !Ä@úÇKõG ¿ù›}mm+Þ[’)Þ¿úÉ ¯þW’Wÿ+Ék¿HòÎiº6WÑ…šÊl;ÇŠ‡Rûßñ e“Š7ãÇô/ei[á÷2l¼TŠüÕ×û'5ré‹*Nl ¼²*<¯ºÏê³IÅŽwró‘-³LŽ68îãAz¶¹tnôƒCc«IÃÝ;¢1ùT›îÍ.¹”ÿÚ7/ËÑp´ÁѧۄêtfœÄà‡|낆FÔj@׌ýïœç'2~ye?±VÖr#„ø Êãå¡´owmòcN¦×£¯­”¸ôµù'—SæÛŒŸc‘îõxåç z¶Vgrs8xŽ*&{‰L¯‡†NÔn@Óü븼§¢<^ºtUÝ¥ÊZí­nÉv¥ qjé-©g`‡5í. S z¶º,ÿß4}m.¡z#?@:Ó§0C©.qs¬Šã6¶š8™¡û khDíÆÊÁo‡@*©ƒ§x’p˜ªU¨^cìq~ ›p¢ÅSÿÔÇŽwj'–MØéѵÑAyÐtŒI¤©ô…Þ±Î!Ü ãdüÛÑ1:­&š\úvºŒûÕï„-›Tì^ãéUÔŒ×i<ºGã»=œ´"T§³{ƒ¦AÓt+¨‘ìðèXçP;Ù$Ö¢÷û»dŠŽu¹´¢yºI°¦òySÝü‹VÖM4ˆ-½Îžm.fÀÓ¹Ñ%T篾^I#j7H¼dUŸØ£âŒÞ°ý “K*ìˆÛÍ)œ,ÅæR4múÝLô¶y¼ôñbeÔMÈ%*ÿÙ¤‡n MÖWHjLÝ>èVîbó8åcªû=b¤:”º¬Õ0,?fb{oeÐt?&éeß2›^ɲòÑ Gè¬z<ÜÅAÆï±¯|$C¨^£g›GÃÑï=‘¡u¾çÂîÕÁZ÷Ïpò_…ihõ“ˆÞí.º3‰Ö¨g°î¹ -³ðl IDAT,f_ìO“ÞñNŽ]ï9¸YEÍ8ƒî-._É2ýœËJS”ÁûOe8jžÍ´³ŲfŠ?ý*AtŒA|—ËÚg3œ|uÈO^4صÒaùC)b-vTcíÓ¦`ÒÉþš@^Ì¢\EßNú)`iҊɑ҉Ej>X¼¤2>J¼¬4*›ª§A¸QÇ jìZípt~±¬Î .ž«¨™àWàlÂ?k0òÉý®•¹òCP3Þ }ÃôsýÊ©<ÿMÓÿÛì÷×ò™‡TN!F¶Ó¿ v¢ÁÄ“*ë{&îwµäò]ëžã>äï“h÷XùHš“ÿO˜æé~LÚúFŽåÿ›¦e–UL€»=ÎúfÝô“’7“bò‡mæÍÞå¿OÓ¶"WLlÞy0ÍØã,f}"Ê“ÏÜØÇ¸9f±å¦o‡Ëé_mÑqÒŠ§ÜÇŠGÒœq]]‡ÝkV>š®Hlís¯ Ó4ÍDyðö})V?•ᤫÃäRŠ•¦™qA s,еÁå­{’´kaå'|ôls9í+Q¬Più‘Ó¿EÃà¤MÀàÐã¥ÄÌþ$±[þ”¥sƒS‘¥Ï8/È1çxï±4};\¬ Ææ?å˜|ªM´Ùo"7ÇbõSiÒ=6]›]’•¦>Ýæ•ÿLðÚ/´g±õ,ÁÚÃßZch«]ëß”+„8rtnpyë7)²I…›UœüWabcýVŽuF@ÃÍQ4 ~"’h÷Šû]JrêòoŒ/ëb¯?Ê`ýóþ¤ˆLŸ¢w‡Ë„9;W:ÅønÐéÞâ›h‹A´Å?¾ЈŽ1hlõ[PP7É Ýí‘êöÕùû¶Fck¾ 4k²òáJAÏV7«0m]ïû¯¥0¥o§KCÔ\ý;¬UÄz‰—‡Ÿ$6Clê™Ü\þÓ\ö¡ÖM˜ršM¤Qg×{Nf2XQ¡gœ`çJƒŽu5ã f~4ȦW²Øù³ƒP½Î‡¯°eY–Änéç1L¿{ëp±a³¿ÍHj©G²q2™sYåÁÚg2¼õ?)þm„PN&®Ð4ع"Wñ˜‰'Y”Ï3Õ—NÆ ¿ñšpCé>3 Ç fâ~6ѽͥwgirÝ$ߪX¨®òÏ PL`üçÉwÿ—M" Õ–Öˆ4é(œ´"›Thº¿GyØ{œ…(=WtLeWÄË¡!‰Í›z¦]üšá¤O‡ˆ4êäRŠ™ ǪÂ`Ü13LÆŸh•bá ŒN´{äÒ +¿X÷f0°Ò×ë¸5ÏoQ¯ˆÕeÏ­ÿ×[µØ/‡Äˆžî}ÿý÷óõ¯gžy†lvø¯w$|¦n³› áayþU«Vñ£ýˆï|ç;$“Éa)ƒb?4˜q~¶å9z·»Œ™iˆé¼·$CªËoiQ~Òd4&œ`±áŬ?:Ÿ0ônwIuúÚfžÏàæñÝÛßÎ1îx¿- v¢A¬Egí³íùçQйÁ)µÈʨÛÄÍØÃ/D#ºÅfñâÅ,[¶ŒóÎ;1cÆ0yòd®¸â .ºè"&Ož<ÜÅ;âhhÍA3:¤g‰D‚gŸ}–ßüæ7,[¶Œööv&MšÄ²eˇ%X1R5mÐØj²ú)&Ó©Ÿ óÎ)ž¹1N°V'—TÔM2h™õÁ¿ŠŽûD•¤yí¶VHÃsüu¾N¼òÀcÃ@íGµ ²IÅóÿ'—R4L1™š\¬é0粫MóòÏbNZnÔ9éÓáâ1m#LÌn’š!6¢€Ÿüä'†ÁþçòÚk¯ñÆopã7 …8ãŒ3¸ì²Ë˜;÷Äá.æˆw¨=º–$lÖÔ¢{‡bíÚµ<ÿüó,Y²„Ý»wÓ××G<Ç0 Ž;î8^xájjj†¤,Bˆý;é3¡ïŸwm)ÁˆÕ9ý«r)E6¡ÖhÅñ-g\©x¬Òøè~=/$-Ç™´ÌŠã™Ô8þÒÇ}2DªËà kØ!­¸ý˜søðç#¿k:œ÷5Å 9å4›)§ùÜ´¿ìF ¦U$T§3÷Óaœ´"W"š?¾F©Yð™–!‹—¢Ò '6wß}7¯¾úê`–éÓ§óöÛo“Ífikk`Æ Ü{ï½ÔÕÕ1ùü4-§‚ô§?b™ºMȨÁÐìƒ{àȤV¯^ÍüÇ­¬[»]×I§Óý¦2ÖÕÕqÎ9çð£ýèà.ĬübÁŽãÉdpOTÃ6Á i¥)ЃD7)-ö7ˆ-$fPÃìc{@#RLh„ŒØÁÇK1¨5±¹üòË™7oÞ`€x<Îý÷ßëV.͋Ű,‹É“'1þ|vLÇNúó‰LÝ&¨Ç°ôÀ  Ä;S§Nåüó4–Õý‰Õ«WcÛ6ñx¼â}WJ1}útN8á„!(‘Õ¯xEoÏË'5iâñ÷v= söâ`ÏË9^Ê$©A5¨‰Íرc;vì`’žžN;í46mÚ„iš455‹ÅX´h‹/æ„N “IÓÛÛËwÿø;œÜþ9m}#GÛŠ']}`ýʹ„Âs!+ݧ¡a!zÄ_hoˆ+šaœ±à þòÒ¿¤¹i ‰D‚¥K—òÀ°yófR©|ï{ßãÁdÁ‚C[@!ªPykM.—#•JÒÓÓË}Ïr§LCÃÎÇK}â¥Ø»=Ʀ··—… ²qãFÎ8ã ®¾új>ö±ÑÜÜ\QQx®*]ê²ÿM¯dItxÌùTS³0õ ¶FG²šý?~<_úÒ—¸îºëp‡_|‘ßþö·<ñÄœþù<õÔSÌŸ?¸‹)„8‚˜º="ã¥(щ͋/¾È­·Þʼyó0ŒÒ ¬á\BÚÉ( «rá&' ¦]ºO¡Èô©â*–Xe_²›õWÚ´ÂÁÚ²)È¥üuÜôír‰6éƒëÊeâþ¼hÓÀ幤"Ùá?O¸®´X•ç®EØmFCÇ úÇPž"¾ÛÁÉ)" fñþáfÛ6ùÈG8÷ÜsX¿~=/¾ø"sçΕ™QBT òyHŠCŸ–}HàDÐÒmL=„­Ð ‚%›±Ftb³hÑ¢á.àWD]³05‹gÒÉI—Õ3aV$?â]çÁïnfÁÆÐÜ$Ñéðâ»È¥œÀ A&éÑØj2÷Êf@#›R,ýa3ΰfi–@Ɖ—‡¨ÐdýªÇÒl|%K¨Î_ ³efå[ùÞi¶¿#ÕIvx„ëMN¾²žºæ0;VæØùN7Êóxîç»@)æ®åÂooÃ긮"Ýç2}A-Ç.¬¢¿ôkmm¥µµu¸‹!„؃††¡Yš…®™èšŽQŒ—óP(<åâ)…ÿ¿‡‹çîºÈÐ, ÝÆÔ˜š†^Êcdîöˆ7¢›á£aj6–ÄÔltͤp†¡Ó‡AÀïSÀšziœàäÅ€ßE”KyغAû† «îá¬k&Ó<5H&áòò];Ùø´Á‡>=‡N’Ý« ®‹j€êò®÷6/ËrêgÃ4´štmtyý×Ibcu4MÇÐLZ?`΢¦ncòƬ}Òá/®2ùÄ }»úÚ>|es±¢æRg}y¡˜:6fxñW;7#DÍXå/„ÈÞãåÓÑC30ö|h¡%\)?ÑQžçâ–%=J|âSˆ—:&ºfbà'dÅD¦l5aQ=$±)Ò°ô–ÄÔƒhp¤aétnNÓ¹%KÃ$ÝÐDý—õ¯öÑ2#HÝ›\ÚC74&ω±ú½.Š tSïaƼFÆúg0ž&£ðð<(ºVu3fŠÁ¤éhšFlšÎ¶©8i¨µüÁÛшw9tµeÉ¥r„b[ÞIì³ìVPÇ0íÒ¤ú\<‚1ƒ®mYIl„eJñÒÒƒ żq C3¡pýÈÂÓ**ã¥òð”¢|ŒR išòO5¥SLÀö'#‰LU;â €ÁÖÃhúbJÓÄÈ¥=þøó6¬Î´ÓbL==†ÔIv¹ôìÈòÔOwTbc-ÆLõcàîõiv¬JѳÃï~?êÄ(ÍÇ䯙çºWûhßFÓ4ÆÏ 1ñøH1wyïÙZŽ Ñ±)CûÆ4GeìôÕáˆMl ÍÄÖ£Øzˆƒi6µBZÅ}²ÛÁsKé} j0÷’æ|¢ž¶÷R¼óhž ³Î«%30ænLé€4uêýÚb+Â=m•×ÎêÝYš–I¸¬y±—ùŸo¡¹Õ¯Üë^îcÛò}_WiíK}Äš-Nÿ+¿|ž«Øòö¾[y„£Ÿ¡YôÖAÆËÁ°}e’ºñ¶ŸØämy'Áx'̘©Aâí/þrÇ_GóÔ ©n—xgŽfüØ÷êï&Þ‘còÜ(ÊS¼óx‰.‡é ü±ƒ›Þˆ³åÏ Bµ“æDÕ|Â&ËÐŒ,G\b£k&!#Š©}°Œ¼å˜;פ8êdéïªüâÏÄ]QÃÔ˜0;ÌÎ÷Ó$ºü‹¼ŸbÙoÛéÝ™£¦¥4F'“ð„¼)·yZwí"Ýë¬1ÈÄ]v­Kmò©i†©a‡ücºYÅÖ=º¡jÇÚìZW¹˜¡akX¡R9¶¯L‘KKkG*?^Æ0µ‘»œûŽ÷R4LpÌ5Ù…R°suŠï%¹ð[Öø_wÑ‹×ïoç˜ù5hºŸ¤c§¶¥ìÁCù Ä`5‰¦íû,BC'hÄŠ—ÿ &æå;wóô-m¸Yº 6º^zîçoÛ…aiÔ´X¤z\:·d8û«þ¸—É'FèØ˜áÉÙNý¤Ñ&“¾9Âõ&§}¦ù€Ë0å¤[ßIðÄO¶Ñ2=Ddž4M­AÒ}þj[vXgÒœ/ýjM­Av¯K1nf˜Î-™â1ÆL ±ê™n–þûv‚1“.m⨹Qž¿½—î܉ç›SÔŽÍ'KÞº®iùþï²!Äá1Tñr(ŒŸbå“Ý<{kãg…9ê¤Á˜ßêÒ¹%‹6Xý|o1~¹Y…ë(Nñd°å˜êêzÒÐ$^îCÕ'6ûS5z„ ¥b¤ÙÔÜäcÿ0‘+S":M­AŽþpšºñþàÚ3¿ÜBÇÆ Én—ؓƣ]K'^ÜÀÔÓbtmÉ¢<Å´Ób4LòÇÔX!…_h!T»ï¦PMטÿÙ1´­N“MzÌùh=º¡‘Š—–{I#=mYºwä˜yN-¡˜Á¤J˜³Ã:¹n‰N‡Tw;¤Š\ð͉ì|?…Òi˜ ޑµl[¸µ|%VˆÁ5Ôñr°ì9ÃÚË•îˆ4˜|ôï'²éÍ8[ßI²ri7º°ž©§Åð…ikÔ·+ŽÑxT;\гû‹¹khº¦´Šÿ%V–TubSx# Xš!(¯0¢fã _Ì05&_:“)ŒcfÑØc÷ý׌±¨“ïŠ*c£ëÍùÁpû«š®1vF¨bß@¬²rÖŒµK³™4ìwÜHƒI¤¾´à”iû]hàÿ kÇÚWC¤Fk M÷Çðfhèų¿žjH=b𪧑—ž«Êâe“9•¤vœMoÙ˜ÂL£k[–‰sJ'jf@£õ/b´Î‹±aYœ÷žéaêi1jZ,]ÍSƒ„jó_we³¤ª‚îžSŠ—:FY+w~?M’›‚‘±¬ì!Ð4 ]×Ðu1¡)Åûݬÿ©U#àlCTR–¤Êgi…œft]G×ý©˜~÷^éLD*¬‡¦ØmèºNs¨´À¥—Ÿ{0ì«`ܱ!v­Í°ñõ8»×¥yý¾vÌ@é«kãŸâly;A¢Ó¡wgŽ«S´L÷O'¦v¬ÍŸîmgËŸý}v¯O³âÉîáz9­/3åñÒ+—þ÷Ÿ††RJb%UÜbSj­Q?әɺže¤Ñ€…£2˜Ú>®9/†œ²ShPÑuV£& :†n`z¾Òúïëp^BCˆÑ <^jù/É‘Ylê{€tÒ!b›#2^6N pâÅõ¬5Žç(¦/¨!\oªó[šÍ Îº—ûè{´‹`Ì eZˆióý«ûjºß½¿òÉnÞ®‡lÊ#\gV̰j˜(ŽÉrÐÂ]ˆ—é„S¼/æM@× tÝÀ0 ?±‘xYTµ‰ PÌT -6cÇ·õ´e‰6XdUK…öh^•ÉyÃE™9ôHôµûg":1m–eaš&¦a[n¤µFˆÁQŒ—š†aLª mþ¶î¶ ‘:sDÆKMƒ)'G™rr´XŒÉ'”Ö¡™ø¡0g‡+W ¦ÔÕduæ|¢¡Ôõ´GWÔ)—7Ø Y*+‹Îæã¥ß¬¦+“˜6®/ ÃDË¿¯…œ#=fV}WøÍªºa0µþdÌü¢u©^‡xgP¤U7îÒÊ~Ž@ó²ëOd¸¨š.Ðü@šIøïI,3™€¶,,»TYË+é‘^Q…8T…3y]×1 ©õ§bèþ¼T·C¢Û)Õ…‡³Ç£ðx9 jºAƒ®¥xÍL!`±, Û.œ ú­7 ãl J›òS~‹iÔ…Æpæ¸k‹ûµ­Mâæ®Ê‘pw‘UɽŒ¹Ñªð§Šh J¢ÚÑ-ÿÚYí[ü5ttÏà¨Äƒ! ¶ecY†á7±J%âÐâeq,†¦c&õá1,ûùâ~;V'p…§în²*±—17Ãû†!^eÒ*”€Æv4S‘Mztã¥Ekê|Á@˲±-+"('UÛU¸îÎ@L,ËbþøËYÙù4ÛS+ÀƒîcÜô0áZ“´×MšîâÏ´‘ ì­ýS`ÓÅÝgWêÛúÝ¥ö¸±Ÿãí³©V ô«ÓÃ-½¯=Ç® ©âèþ–¾ù4&‡ …B‚ì@Ó2‹]ŒRa…ø`Êãeá¤Á²,L¸š•¤-½<Ø”—¡“´×CšôÂõ™$^VÞ8,ñ2†WŠ—»sìÚ˜*–}\ßꈄ#„Âá|rã·Úèù±6…ÄGr¼¬ÚÄÊÇØè˜¦‰° …Â|lÒ Üµî+dÜ^À‰{pTª8j>›tÉ&÷L,†B0×HkßÇh²Ž!V£¶¦–ºº:¢±ÑHÄo±±m,ˬ¨¤RY…8t…® ?^Z„‚AœœÃ„ú©,Öoæµ÷ófò^\2Åx™Iºd$^‹`¶™ÖøÇi¶[‰ÖĨ««£¶¶ŽX,æŸæÇÙfåxÄ#=^VybC±?Q×5,ËÂu„B®ë‚RÌÓsL÷B¶ô­`gú}:ÜõÄÙ…R®?¸¢/t$Nø«Vùþ|evÆsÆcuÆ$Bµ!"áÑh”ššjb5ÔÄb„#¶må×h³!S)^ê~¼ô„]ÏóPJñÚ§˜Þs6[{—³#ý>îÚ.<å‚ÄËè/~¼tÇS£&SkL \&‹Å¨©©!«¡¦¦†H8B0²- ‰—ª:±ÙólÞ0ÀøÓþáºa`šÑP-G%O N“ÉdÈ99\ÇÅU.ÊSþ#ò‹ý‰CåÕÛ?޼'BªþñÒÀ¶m”§P(4Í¿Ï2-b¡ZŽJÎ%N㥓sðð$^ºÂjÁh` LÃÄ0 v€`0H8ÉŸ ú'„áp„P(XÑ %ñ²¤ª(õkùE§”Rh?±Ñu«8H.H*•&›ÍÉdpÇqp½BbÈ_±qåÊU´¶M0ÜÿÎì|ÆZ¡O¿0`1`ƒ„C!B¡?º?$`ŠS½åìCˆÁ7P¼$à'5ű7¶M0"N‘ÍfÈf³U/«Iy¼Ôt3?kÍ_«&@($ògŽC!‚ @Ë21d’E?UŸØ”Ÿ…Fñb ‰„ýJšsœ|‹çyx… :Â+êmÿu³fÎdlËØá.ÊþÏJk Y¦‰iYl˶ ü©Ý¶ešÚ3©‘Ê*ÄàÛ3^ú×góã¥m†Bd3~¼Ìær8N/ßeU-ñ²ªì/ Ã_tÏ´,ÿd0 `ÛÄKiÝȨHlÊÏB*. ¦iþÄ´lœPˆ\ÎÁu7ŸÔxþ¥À#ÿ Ĩoh ©¹y¸‹²_š¦Aþº4…¥¾õ|«išX¦•_Áï*,$4RI…8¼ö/uÝ(ÆË\0ä·ÒTi¼¬&ÅK#¢×?^–‰—‰ TVÖÂ\˜2l˜žkã8žçây*ÿ¿‡Rùñ8ùþâ‘\Ym˦¶¦–úºúá.Ê>ùUø]ÓŠg„…åÜýj¢:ºV¸à¥TR!†Â¾â¥i™¸Ž…m»U/«Å¾â¥Â矠†QŒ—š®å×­)?†(5‰ ô_«¡0[J)O÷0MOùƒß”R~EE¡UI5M“HÄ£1^V•ƒˆ—+÷¯ ¾!M©ÂVþ^º]=k1hšß¯ªkUpQöø“JbdMñ²ªH¼T£>±)Øû‡Bú‹'©ŒBT‰—ÃCâåà8b›}©¦“,Ä$„NÄHW}B!„F!„BŒ’Ø!„bÔÄF!„£†$6B!„5$±B!Ĩ!‰B!F Il„B1jHb#„BˆQC!„BŒ’Ø!„bÔÄF!„£†$6B!„5$±B!Ĩ!‰B!F s;¬Y³†;øûôéÓùÜç>×o¿»ï¾›+VŸû,W\q‹/æÉ'Ÿ¤µµ•Ë.»lÀÁÈB!„ë%6¦ir×]waÛ6×_=Û·oßë¾·Ür ›6mâ«_ý*µµµÅûmÛæ›ßü&ñxœüà¤B!„>ð}³gÏæûßÿ>ÝÝÝûì’zíµ×8õÔSûm›7o¯¼òÊ-ƨ¦”"•Jíu{2™ÂÒ!„#ß!­<ü­o}‹SN9…Ç{Œ»îºkÀ}Ö¬YÀøñãûm;v,š¦±fÍ”R‡R”QIÓ4¾÷½ï‘N§ûm{øá‡yæ™g†¡TB!ÄÈuH‰aÜy箻¤ ³¦:::úmëêêB)ÅøñãÑ4íPŠ2jÅãq,XP‘Üüþ÷¿ç²Ë.cÁ‚ÃX2!„bä9äkEÍš5‹ú§¢»»›/|á ý¶Ï™3€•+WöÛV¸¯°èïSŸúï¼ógu®ëòôÓOóÙÏ~–ÖÖVjjj†»xB!Ĉ2(ÁüÛ¿ý[æÍ›ÇSO=ÕoÛ×¾ö5¢Ñ(·Þz+žçUlûÙÏ~†¦i|ûÛߌbŒJóçϧ¡=tö IDAT¡·Þz‹§žzŠo}ë[ôöörÙe— wÑ„Bˆg¿‰M&“aÙ²e¼öÚk¼ù曼úê«ý­º¤‚Á`¿ÇOœ8‘Ÿýìg¬ZµŠ«®ºŠÍ›7³k×.¾ýíosï½÷òƒü 8ˆXôgYS¦L!“ÉÐÓÓCGGÍÍÍ\zé¥Ã]4!„bÄÙob³~ýz¾ùÍoâyáp˜n¸åË—÷ÛoÆŒÜrË-,\¸°_‚sõÕWó§?ý Ó4Y´hgœqëÖ­céÒ¥üÃ?üÃཚQêÊ+¯Ä4K‹DÛ¶ÍìÙ³‡±DB!ÄȤ)¥ºÚò;Ï<óLž{î¹á)‘ègË–-œrÊ)ìܹ€‹/¾˜ßýîwÃ\*!„Â7’ò†Ac#¯I“&‡‡Ã|æ3Ÿæ !„#“$6Uâì³Ï ò‘|d˜K#„BŒL’ØT‰+¯¼˲hll$wq„BˆÉÜÿ.£G5¯n<þ|<ÏcÑ¢EUñ:dÁE!„ÃaT'6å €rvã¤ßÂsÚoïÁ¦OkáãçÉtß9ÜEÙ‡MÓд F`F`hÆ—K!†FOOOÅ5ëêê\ÖäÝwßeÛ¶mÅߣÑ(óçÏ’2Žf£2±)$4¹øSdºoÃM½Ž—Û2Ì¥:tÿô7ŠMø1ɽ_L½*hz#0+zÁÆ¿C3j÷ÿ !„¨]]]üÏÿüK—.eûöí´´´°zõê~«Å¿þúëÜ~ûí¼òÊ+̘1ƒK.¹D›A0ª¦{åõ’lû²#ºeCèÖD"ãïÀŠÊ€h!Äèrã7WÖÿüç?Ï/~ñ‹~û(¥ƒ¼òÊ+Ì;w¨‹8hFRÞ0jZl I“|Ķ«ñr[‹Û4Íİê0ÌZ ³] W14rFÌÎ’(<7‰ëôø?¹nÿ¾ÜVú6O áKDÆþ;h£æ#)„\xá…¼ûî»ÜqÇ,^¼˜óÏ?¿b»ßM¯aÒ=?XFÅ·H1©I¯'¾ùã(/^ÜM 2MÆt ?K“p©øŸqs]d:ަ…ýÉ0P!Wmm-wÜqçŸ>×\s Ë—/?¨ oÚ´‰7ß|“x<Î 'œÀqLJ®Ë„æ}©úĦÔxžK²íóŤFÓ,"uó0̺=öò"îA•ý¯òÿT±`ªp?äïó喝\¿cPv UyŒŠc«ÊÛ ðØâóì±ÚcÊ÷/¾†²ÁÙeϯ!,;†m×b˜¢u§“I®'X(ÒÿŽ]sfXú˜…£ÇyçÇ5×\Ãí·ßÎ7¾ñ n»í¶ý>&sýõ×sï½÷²xñb"‘ßþö·™4iwß}7Ó¦M‚’W§ªNl Iëºd:ÿ/nò…â¶HÝéf¬lßÔå£TYb¡ÊU–Xô¿ÏO,öxÜîˆ=´ç§â±ºf­›F(ÜB ÜŠR92É5€G|Ûg©úg4=24ï…B ý×åÉ'ŸäöÛogñâÅœ{î¹ûÜÿŠ+®àøÏ?ÿ|qFÕw¾ó,XÀüùóyÿý÷©­•‰©Úö¬RK‡òRd;~XÜO+&5žòÊ¿£åçà~TùÏA<´ìݪ¸Çórôt®¤·gsþýš^z¿²ëÉtÝŽBŒ&±XŒ;î¸MÓøë¿þkúúúöºï#<£>Ê•W^Y1M¼¥¥…ï~÷»ìܹ“ïÿûCQìªTµ‰ øÉçyd“o£<ÿC¢i&Áȱùí”}§Ž€ aÐ~†ÑÁ$D ”àø4-@&"“îAÓ4‚Ñ9ÅmNYË›BŒçœs_üâÙ¼y3ßøÆ7öºßþðÀx¼§E‹°dÉ’ÃSÈQ *›â´n¥ð”‡“z£¸Í´›),×<ÈhQ%ÉQY‚Sy§†žûïÛ €iÕ¡é6Nê­¡)ŸB ±›o¾™£>šÛn»¥K—¸ÏòåËÿÈ{jll$‰°víZÒéôa-kµªÊÄJI븸é7‹÷û‰MyR#6„IÐÉnÔPÞåyŽ“(ôör›PnÇ¡?¯BŒ0Ñh”_ýêWûì’ŠDü1†™L¦ß6ÏóÈf³Ø¶eY‡½¼Õ¨*•¬êyŽë r«‹Û ³¾°×ðnì÷JKCx)&m dG;Ȥ'ŸÜhZMWlÊe“fi œ›yïK-„#ÓÂ… ùêW¿Ê¦M›ø»¿û»~Û?ô¡°~ýú~Û¶mÛF.—cæÌ™²öÍ^T]b£”BÓ4”§PžÂu\rí+WBŒ;vì ­­­ò;*/»¤öÜ>{öln¸ážþyn¾ùæâý/¼ð7ß|3§v_þò—{ù«UU&6žçáz¥Äf9ÍŽ¶4›¶$Ù´%É–m)zzs‡¿ݬG«îe“„‬[·Žh4Ê-·ÜÂsÏ=G(âá‡î·ßüùóùú׿>à1~øÃòßÿýßüüç?gòäÉüñ\|ñÅ\{íµ<óÌ3Ò µÕ÷M£áwC)…Ržë¢«ëù—Ÿ­¥mWå`¬©SÂ|íÚ©45؇©ÀÕë•—“ŽpÅÅ/˜—sàïÔK(ÿø·Qzt£M p*„ÕlêÔ©Äãñýïüô§?å§?ýé€Û®ºê*®ºê*:;;I$Î’ýU_b£ü‹†ùƒ‡®ç¡)uP½]0–æ¦O?¿›u“ü÷ý[¸î SI¦\¶ïH“ÉzÄ¢&Æ1ŒÒ•‚­ÛSÄ õ6cšhùÍٜǦ-I”‚±c‚ÔÄúÿi]W±kwÀ†z‹­ÛSDÂ: JÁŽzã9ƵØÔDKÙ¸´wæèîÍ1iB€¾>‡LÖ¥¡ÞĶ`çî®ç1vŒ…tu;¤3 uv~ÈQ&«ØÞ–ÅóÆšØ{äqí.í]±¨FK“ë*vw¸ôÆ=:»]Úv¹ƒP«üC·íréíóÈæ4ÚÚ=4cšôâs¶µ‡ð”ÅØfpH“¬F!RCC Ã]ŒªQ}‰ ùulòÓ¹ê»ÜŸY3bÌžYƒ®Ãm¿ÞÄÖmþtã¿ùÞrúâ¥A«cšlnøútZš¼¿6οܺ–dÊ-nÿüUGqöMüþ±üþñ¸n©,·Þ|®¸d<óN©ãß¾ ›“ùU^žÓÄå·°ôÜ}¿ßO;¦ÙÆ4`ÛŽ ß¹~Ç´ùáO·O¸üç¿´°à®{Ûy{E‚¿ûÊXfäÝU)~ñ›vúú\@ ëüŸ+ê9qv¾¸Çþe'Ý=¥×µð´ÇÏ´¹ç!ÿŒãí•YÞ^‘eîñ6_¸:Zö>À?ÿ»?U1Vüà'q, nùçËßs¸ë~—xÒâØüåÇjYø•3¢„BˆÁT•‰MßU¸fÑ{ge/Ûw¦yö…vŽšäÙþõ§bÆ´(¦©ñèÚøß%m<ò‡6þúÓGñôó»I¦\~ô™47ظ9I4b¢,yz'ãÆøþ7%“õxouÁÀÞû?;»rLä+=…±M6¿¹6'¹ø£-œ5¿žŸýr3/mgæô0S&ùŸßïDÓàš«Ç“Ëyüò·;øµf²ŠÛÿ»TÊã›_C4¢óÏÿÖÆ/ÛÅÍßká•דt÷z|öŠ:NždÇΩ´ÇÄqŸ8/ÂÃOÆ™uŒÍ…焈E+[k4 ¾öù·ü"N0¨ñåÿB²YÅ]÷{Ä“:W\\OMTçÎû:¹ï¡nfLµ;¦ª?vB!F°ªý†9”ñ=µ³x{ÖŒW-ž@gw–ÿºk#7'I¥ýŒ¶]þÊŽÓ§FyiY'ÿø“÷9fj„“æÔqú©hL;:ÂÛ+zù»ï¯à¸ccœ4§Ž``ïã²u]ãËŸ›B4b€R,ÏoY¾ªU«ãttúƒšW­NË)Z§9ýÔZ”R<¾´ƒ¶]Ù~Çõÿ&•ÉÇ–mYâ —`@ç¡'z0 dÊcËöÓZý>©_ß×Ãs/'8~V€¿˜$Ñ?ÖÿxÔÔè̘j1P ÙôV]ËßV°~k˜x²›æFƒ3OóÇݼüºÍ»«Ò¬Ù‘ÄF!ÄaSÕß0…ißôÿ>ß§k®>Šc‰Q_g°ýd˶wݳ…ÉB\û™)lÙžâ·nEyþc>²°™)“Â,{«‹•ï÷q×=[èìÌrù%¹þ‹SùóòÿçÝ^^ZÖÉ?ÔÎ`Æ´è€ÏßÔ`˜Ò2Óô Lk„HÄ 0uzòø šæï“Ë•ŠœS™\Æù]aÛÛJIi–þŸ}lPÌ>6€êj Æ4üó cxýÏIV¯ËòÐ’8ï¯Íð·_¬çàéèf}þ9»qÊ–¢Éå' ™†LéBqøŒÈéÞ[·n¥»»û°¿©Áfì˜@1©ØÞæ·ÌD"¶­ñçå=yæ…vº{sœ~jgŸá_¶¡«Çÿ¶þýc;ˆ„MÎ;s ³gúW©îî9ð©ä'_@GWŽéSÃ=9D_Ÿ‹Ðh"ÒÙº=Ãïoçþ‡v[t Žàïâ®{w³»£”QLœ`ÓÜhOxhÌšdL“Ŧ-YÆ4™¼³2ͺMYf䂳#˜ôôùÙÜ„|‹ÍšuYžy1ÅæmN¿üѲ4šu’i%Ïøó ‡ ã,ë ºz|¬›§þØÇºlKcÆ´Àÿ]Fgg';vxBˆÑiD¶Ø466rÉ%—°qãF>ùÉOrÙe—q ' i‡ïlÿäê8~V ï®êå¦[Ö°ð´&V¾_º†ÇÖí)žúã.<Ïo!™:%ÌEŽàµ7»xè‰6BA“O¨ãÔ¹ÞâqÅ_Ž'›õxõõn^}£”¢¥ÙfÞÉ5DŸ¿j<ÿõëmüïã»™15ÌÑ“ƒlØ\ºøÙÙókÙ´5ÍÒç»™1-ÄÔ)Ömô·›†ÆW>×̯ïëàGºñ§•ÁÔ£ü.¨ž>ß<ØM.§Ð4;ÆäÒú-McÇ,˜â­w3Üó¿ >yaˆÉãCýÊ¿è# ,}!ËÃOô0õ¨'ÌóÅ¿jæ®{;xò9ÿoX_§sÅ'ëh¨œµ”R¼ñÆÜsÏ=<òÈ#Ìœ9“ûî»oPŽ-„¢ziJ©n ¶üÎ3Ï<“çž{nxJ”—N§Y°`o½õuuuæÌ™Ã§?ýiÎ;ï<"‘©TŠÞ¾^ìžЗ£€š†sÑP¾›êàŸ7žpŠiÞÙœGoŸC,jV´ö$’.É”KSƒÍ¾ó/U6à¹ì¶R¸®Ggw–PP'ÖóãYüŸ\Î#•v‰Etnü›Yù~¢8+ªpŒxÂ!Ö‹ÇUq;•òg)ÕDuük§ùÛWÑÛç`[k¥Ç–=ñvYÙ&ºQƒ†YÀ­ò·òû'R.ŽãQ-=Tü ×µMm&€Ø”g°" +þr,Y²„»ï¾›åË—“ÉdèííåÔSOå™gžÁÞs»Bˆ!1ò†‚Ùb yþùç9묳xë­·hoogÛ¶m<ûì³ÔÖÖR__Ï\À99‡'Þóúc_f[ú^ò‹„ "áCk0 ¦F»˜”3MXÔ`_ÙZa0òÞƒ:Á Öo&™a@}­?¶çÀf˜ihFM‹øÈÜÇs†C:(­ßë9žçñúë¯sß}÷ñè£ÒÓÓCww7ét:ÿz‚œzê©<ýôÓ’Ô!„ö’ØX–żyó†º,Êf³8e£PS©©Tж¶6Þ{ï=þíßþ¿89­ÿLž0ŒB]ÐÈ™§Õ2®eè¿Ì5=ˆ¦Ç@Ó÷™Ðª÷Woæš/ŸÎ«¯¾:àµTÀÿl¤R).\8À„B •éÓ§wŠLlžzê©¡.Ç€:::8ýôÓ‹_jš¦ÑØØH0döìÙ,^¼˜S?|*Þ•Å®¨ªðÁ0ŠŽ=&LeWÑá~M  Q(v;Þ¿öŒé“yå•WèììdÉ’%üú׿fÅŠ¤Ói:::òeÒH&“¼ôÒK²*§B`wEµ··3wî\vîÜɘ1c¨¯¯ç¢‹.âÒK/eΜ98ŽScCÏþ'>?¡‰ ð×±”VšƒH¬Š×KQJñúë¯sÏ=÷ðØcÑÝÝÍúõë9ùä“yýõ×%¹B1ðàáá¶{÷n/^L8檫®â /,~i)¥ð<\.wX~{Œ«?±“dÚåâEÍØ–ÖoðîÏtÐÛ›cѹ •ƒ‹óÇxæ…n::sœ{f-µ1GžìúÿÛ»ó»ÎúŒãß÷=Ë]æŽg÷:^RoÇ.!%Aª”ª¥ !PTÆ)RQ mh*!¥¥¢ ZD ÙH ‰…M¡„T”ØŽ—x‰·ÏrgæÎ½gíçÎf;‹I<÷äùHãŸ{|î9wþ8ß÷wÞSõ„w½½Ïå…‹çŒ-‚)eSNL–g>Ï߯ÅÃóŽ=½¿™~=;æK->ÝÐÐ<ð·ÝvI’p÷ÝwÓÙÙ¹@¿'iE‹rÄÆq~øÃ¾¢w¿\7ݼŸ]{ÆùøG6°þ¢³w¾>Wýø#c!ïx[/¾wæµÿèÑŽ¸ú-´•Ï\‚èÑ_Œ³ÿÙ:o¼¬BG»åáŸT©ŽÇ¼ómKðÜÿ,)€-`(d]ÔÏwªëUÒÛÛËŽ;رcI’06¦¡;‘׺EljJaðT@&ôõ­†„AŠåÅyûa±ãuÂ0¥Ui¦ÂÐp@$tuù –F0<àzÇ‘‘€±ñˆC Ê%—înŸ‚?»j0Àh5dªÓÙé1U©V#Öô±‡0)}ݳ2?~Ý:Â8Éž$j:1PoÄô¯8ûÂvƒ§B&'búWyg}ýt“µ„c'|ϰr™‹ëdafp’Ô°li6åtüdß7tu8ÙµD”Š–ŽvËT=áØ‰FB{Ų¢ÏÅ6«M%ŒÇ´• ž‡¬]åáyY©c'C’4eÕ2ß_¸àj­¥«ë|VK‘ËÈXÈW??@¥Ír×÷Nðý °~]‰zcþèÉs×÷²×ûWúá ®<ü“ î¾o”(2€ai_‘^ÛOÿª"7ÿçö¬ñùÏ®'ŽR>}ãV,õ¹áSkyô±*ß¾gw¿³‡·_ÝÉ'o8Hm*™™Nêív¹þºeôv»<ö«I¾ýÝa¶]\âÀ¡“1ŸûärŽù;‡™¨e«Wʆ×t±õb=¦-"" gQ¶TXhcã!ŸúØFV.+òÄÓUޝÇ)_ùæÆÆC>ñWøÂg/ÁZ¸ù[©Ž§\qùJ®~ëj~ýä7Ýü=>Æ[®\ÉUoîçšwoࢵY„¾o#ŸùøvÖ­iŸÿ¦Æ`š_CÃ!×ä·éî*óôž)žxºÁõ}=~ö¿Ôêm`³'ŽÀ‚q9t$àþ‡†(—>ö«Ù¾­ÂÈèl+…ÁS!ß¹oϵ|dçr®ºr 'ç6Æ´Íc9ÛÎÑ“eîøÞ$KÚ¸ñ3|ôCë89pËíGØ”u8ß½·Æî}5Œc'Æ'bvïÊöÙ˜­:¼óš¥|ñ†µ|éÆuüÑÛ:Žxð‘ùÓ@Oí™âM—·ñ¡ôàXÃ-w SRþöº^>ó7}Ôƒ”[î!ïT—ˆˆ´ž\ØLûã?XÁÖ%lÚÐÆÑuöœ$ŠSFÆB ¾å¾“—Fpà°aû¶2;þt3»žç¿¤¯·ÈµÈžÓ_ÓßF[9ûèÖ­®°ià ×^_õ¦å\¶½—G~zŒ‘Ñ¿wÕJ.ÛÞÃC?nã‰ß8Ô`ë@Ƹcl'û ‘â³uKoxý.bx¨ÊT=ÂØ ûL’¦.7TxãV‘ÃÔ‰0n7Ö-aìq Æ–ØõÌ0ijˆ“”[ï¦XÈ Qzºæ×Ëœmñ¹ì\ Aà’¤–81€¦ˆãÄ€C9`‹¤iJ™æ‰Y»â4‹‡—T¶d½ ¶ Tf.`ýEe\×°{_8Nù뻘¬ÅüèÑ1Æ'b.ÝVÁ8t¤ÁíßbõJŸk¯éãðÑ÷|¸ÙY|Öê95?N³þÆu³&œز©0s>""" å51u&ÃòåݬZÑI½aˆbØzq+—•9tx‚¾Þ"a˜pÓ¿ÿ†0JØùþDqÂM_{ŠFп* 8>r„‡|”0Lô 6-Áq Oíãþ‡Žrëæ¼Ç¦õü‚eß³“Ü÷àq¾}Ïa&&¢ç=ÞÖ‹Ûñ=ÃÉ¡€®NMmt´»ŒŽE‹Ï5l¸¨Ìñ“ƒ§6­/³yC™]ÏÔšç“MU̦ÃÚÊžgxrWíE¯eÍ*Ÿ®N‡ñ‰×úz éíV°‘…ó 6Ùt“±íX§Ä__·•Í;¸çÞgù‡ç_þíINÕ±ÆpÛÝû8xx‚w½c-¿ÿ»«xïŸ\ÄsG'¹õö½¼õÊå lêä—ÿ7Ä-·í¡ÞˆôL—öù³÷®#ŽSîø¯CŒODôöÌŽôt,ñØù¾uXkøÎ½ÏqìxU+ŠÏ{¼¾Ÿë®]C{Ååæoá†/ìçÖ»ŽÍiؘ…—J›ËŠe>›ÖÏvòÞ²9{íÒmmlÙTb÷¾)nºù8+_B[Ï3\÷ÁÖö{|çûUþñ_ùÆm£Œ/lY” ô½ó[ Ï€)alé¬Çl1Õñ%í^ó‘íWü*ÎÚ-{v±¼æ>¤41Ag .O_ ” ŠiLÅT*μÅð¦/þl äMLF4‚˜ŽvÇN?œ~Ú¹œ±¸Þü}&&#J“M3Í,ÆÇìû>Ï}õFBm*¡½ÍàyÙÄç»@ŸˆˆÈéò_ccŠSž-Š9‹‚ïÐ׳8§D ¾¥àŸ½ $€çZ¼Ê¹5ª´9ÍþοEÂl'ñsû÷Å‚¡X|á.ä"""ç«¥ƒÍôãÔó‹e§o˜.Ø Æ,ÎÀ"³L³¨;ev:oñ®9-""‹YËר ±éù{gÛ§PÓ"<7›"Œ£ÙµpŒ·âBŽˆˆ´°– 6Ùh ` L·Ìl‚ÁéŸ.Èyɹóüì ³8ÀØvã…<%iQ-lfB k ‘™ 6ap²ùSD.ÃÍ…ž£9Ç÷±Ý‹¥Œ±$IHOà·Ÿû‰ˆˆÐ‚ÁÆ4»N§)Xk°Ö!²[I›7Â$ž ‡³}ÓIswê×:Çq©´/ 1¹kf»[ºüB’ˆˆ´¸– 6išØXk1Æb¬Á8}Œ‡oÉv009úiš1&´^Ê«çEFZL¶‡çéèìÇK Ô6_÷(tí|¥ORDDrªå‚Í\ÖZ\ÇÅóN6ZaÒ1L:„h籎K¡P¡Ò¾”Îîµ8n8ž¢>þë™}J}ŸÆ)\rÏRDDZYK.ÐÇ1Q155ÅøÄ8###$“w³fÉ3ÙÅ:%Ú:®ÀqOë¼½¨ë7Î-x¥3œÿ1^î9¼´ãŒ±ó¶7¦Ò˜|š4Íj¡ÜÒëi_÷3Œõ9-l’$RÂ("h4¨ÕjŒŽ1|êKÌçXÞþßóî©~±¯°ÇëÂÚ‹;Ø,V vÒ”8ž ŽFëÏ…§f^²îr*kÆ)\Ü\›HDDäܵd°ÉЇSâ$& Bêõ:“ŒŽŽ2<<ŒÜËú¾¯Sp'Îø·Æx¸ ¤IÒ˜Óƒ¥·ä=”— ëö)ÔˆˆÈËÒ²Á²‘›8ŽhA6%U­2:6Æèè“Õƒ¬íú«z~‰Q]Í…eæ~› .Æ]‰ßûO;ß‹µÙ4•‚ˆˆ¼-l`ΨM³Ö&&k“LNLP­V«ŽQ­V©×NPpž¦½°îÊAŠþÌ4šT)ñBi.+„ÁÌ´º°Öb‹ã88ŽƒuÊàà–.§P¹‚BÛ%¸®ã8sÚcˆˆˆœ¿–ïe­ÅuÒÔ£T,ac-Öqð<ŸZ©D­¶”“µßáÈX@†DqL’$¤I³ãµî§/‹Á`lö»p¬ƒçyø¾O±X¤\*ÓÖÖF¥R¡R¬P.·á—Kx~Çñ°B;ûñIDATŽU ‘Ó²ÁƘ¬ãµµ–”×ug¶[›|ŸR©D¥R§Ñ‚QO›æ”ÖóuΖ—fîã8¸®‡ïyøŸb¡H©\¦\*Q,•(Šø¾çyÙ(ŽÑ”ˆˆ,œ– 60nëœ1â8n6jP*a†al’˜4IIÒ4[ñ4js>¦ó ÉZ[kp¬ƒã:x®‡çy üBßóñ >¾çÏLMM‡…Y(-læš.>u]·l,žçR,ˆ¢(ûŠc’æhÍt¨ÉFkŒ‚Íù˜Î„†™pc›Ó€ŽupÝlñD×uq]·YgcçÔˆˆˆ,¤–6ÓÿÛŸž–š)ZmŽÚx^BœÌ4i’’2l.ðä€1YƱÍsŽ5Í Óücæ…ÖˆˆÈBjù`3mzZjîÓ5ÖZ’$ÁM]Ò4m.ì7ûDÕtCÍæ.Й·²ÓWžÿÙÏœÞ6³·ˆˆ¼rl`6ÜóÖE9½H8[W£`ó2Ì™‹šùÑœñýô£P#""¯”\˜Ó<}gîvye<_hQ˜‘WCî‚Í\Ïht“}uès‘W[®ƒÍét£É7=s+"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹¡`#"""¹ñÿÚ'2Ç’ çIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/docs/.static/logo_hi.gif0000664000175000017500000000777600000000000021406 0ustar00tseavertseaver00000000000000GIF89až4ÕpolééçèãÕñíáõòèôñæíéܵµµýýüEEE›˜999êæ×äÞÍVVUçâÓóðåãÜÌÆÅŪ©¥×ÔÊòîãØØØ‰‡„•”ÊǼìèÛóóòâÛʤ£Ÿ·µ¬úù÷áÝÓöôîãâჂÀ¾¸ÞÙËïëßøöò ž™¯®©óñéðìàÐÎÆdcaˆíììóïäåßÏõòëïëáëçÙRRQòïã]\\{zx÷õï???éçÞMLLÞÛÐ444333!ù,ž4ÿ@™pH,ȤrÉl:ŸÐ¨‘@­Z¯Ø¬vËíz¿à°xl-Ïè´zÍæšÛð¸|ž~ãø¼‡su2WfoT„†eˆ`ƒU‰‡X…”„’bƒ–o™‚e‚f ¢£¤ :¤7=‡ˆ‹•ž¬›­’™­Ž¬…—±‹µ³¾¹»•³¯Ã“¶¼¶ÈÊ‹<¥Ï¤§5)¿­µ±רÛݳ߼‡ãÚÁ¹½Õ˿ڶܾÄ×ÕŽê¸ôߨ΢5þÿ5ø=C!Ñ·mõ‰+·Mœ>o6dñ×Útïãu³._»ëݘU—n›>¿÷ëþØŸ3:XfCõ™§`[Ü0/h ýu·CPÀÀ¾!¨ÿ[‚ šW d(À‡¾‰Ø]…"XÁö°…®˜…rÈ_ˆý Ђ9"#2²dw¦¹H e.zØ¢„õá0]ÐZ}7ÚÂLðã Pe“Iº¨8tÙO aä“é‘iCÜÀÃŒà W’I -*è „¶ÀozXvê°@5à @’Ùd °ä—"èæ§^0e•™º8€Ô0ݪ¬&p˜ºØ"|¶ê­ŽÐC¬nÊk, ì°ÄÞ௘0±Ì6»€,w)°Ïúðìµ>øàÀ½öjj“wÈi©É‚šé<ÿ€ çºÉ‚ƒ¬:pƒ¦¬ê +œ[®8àêèt¸ìh Çêkê(8ê,³#hð©›,,¬µ/ÀUú"¬C¶ÏêPC—g‹ƒä–ûé áZ6À ù滲˙‚2å®A¸:ÜÐ|°À{J/Ÿ B ØPpÀ Z»™®P Äòp MK À İC»,(Ð g£ öÚ lŒ«°|® !p€'|ð TëƒØí¶¼òÊ)PõàrÃìª ðò àŒÁœPÀàì êt5Èè²Á:zƒ|pBX*„ÂR“3Ë(‹Á ¥£®ÿ D-,.³ A'ÂðÄGü, @8 ó@„A¬*Hà@¶>àPà‰³î q·lË+˜0þ ¬šÞ䯀ôt)T>âÊçÏPÿ`Ë&(0,=#@÷V`?ã- ;h_¦ƒçS܃®Å­—™qçËWªu±ò•oƒÙâ @¹™/S¸^¶&¯ òÏß ÿÊg€ÀéZU’G¾¬ Ñó ù é>À›ÂBÿ^E¥ÛZÿºÑUmyUkº-¶Å}ÖT0]‡–wµóÍí~ÿ;Ü™Î9%pq³u²˜ªxÀ‚OÊÀó0 h0Áœ‘7ºÿ]- ÊKÝ-Àÿì—ÅÈ[^`q< ]rCyPb¦°6ˆ€£Gy³$a>p(%ÿ%@$€ïg- }ÑÇ€•§;°%yÒ'‚Ñç?ÖrÖgyà<2ÈÆTk…=>N•g~'@S2ˆD1Ø\‚4‚l#px¶ô(wÒ\µÔ˜i•×€†e„Bxxµ42Ñ-%eôÄTÕ\? ‚Ñ÷•§05Ph>(}`(&èu$`x†Ð/Ö"ƒr(^@†6ø†Ï†VzÏ#ehy)°iVhK‡—…ng(`„NHˆz¨†!°„•w×ó„ðB.  ‰gx†óò%d؉mȆlè\e-7p_}4zŸ!‡<`P#”K`h~Àv,0\e… `7ÿÐ4‰øÁ(Œ;0H9ƒÐ¤È†Ð<Ò8þ ‘ØŒ°<@ˆÙâ1#@”™Ö‰œøÐT9l(ŒìØŽÂ8mŒîȆ  À$ LC9'J(Œ.`KÀµŽP ùh´s3°ŽóèŽ`"ÀúÈ#ô׎ëyS:ùY: 9’2‘`X`Yùh‘¥ Ù†0i’2i’ 0mÓ2“°0UTŽYXéŽ`FC§“3ɰXo:éH•ÂXyLé“B7à. •QIþ׋† 0–dY–fy–ÿ)ðB¹ –gù–p—r9—tY—t‰•¨îíÈ—~)“|ÙXr(‡R…VØ„Ö ”2€Žx‰Mxu2øˆ“yuðW˜—ɈZø„“É™ æ”éÙ‚¦ù¦™š§é—«‰š€W•ùˆ)™B8^ÃçÀåhB¨™˜y˜‘¹™²)š…iXy˜†¹‡¢i™ZHœzø€š¬©šÂ¨šÔI  pœÃyœÈéˆÕ1|?P‰˜Ë雿I›ÄÙ™Ëù™†y™»¹‡ê Ÿ…äT¤iš Pú¹Ÿ©Ù‹TK#p: Z ÊP…À•` ú aÿôÉ)¡z¡z¡Eç÷Éšùšš ¢ ªŸp?2à“UÄ¢æÃ¢-j-ê“®Bð¢2º£/:£1:£= ŒpŸq)@9åã¢>)£U´£.ZFK¥KÊ£>:¥0Ú¢, 60¢%Ê¥#ú¡$¢bú¥1P3Ð¥øÉ¥]ª¦ùÙ¦Ó¶>i§jЦkú¥m ¢0 `U+ŠšmJ¦€ ¢ ¦l*¨dj¨ˆj§r*õA¨Ý7¢ॆj§” ©v©•¨™Ú DG€©ŠÚ˜iú¥‘Z$€b90¢5-ÀÀ ª ­š© :©ÿ•Jª¼ª«¿*ª›*¬º ªÃj©£J©Æ*ª£Š©KfK ºjª ©£êm2x‡=ÚšrؽʬËJ¬Âš¬Ëj¬ãZ¬â ©Ý×®Ö*®îú¨È¯í:¯ñÊ©k¥8Ú¯ôÊ®þš#ðGÚŠ{7P¡0°€Êhëz¯ÌÊ®ð:±õz¯Öê®K¯û¯û± ¯Ëè=Ѳ °4 y<°”2=7U”, M9P1€²:»³˳>û³ +²@+´;K´EÛ³@›´;ËîÊLÛ®O۴ݵNë¯S[µL[µP;«³ªµ\ûµXë´O+¶W;µe›µf‹µôªµa»µc[¶\K¶]k¶R µY;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/.static/repoze.css0000664000175000017500000000062300000000000021275 0ustar00tseavertseaver00000000000000@import url('default.css'); body { background-color: #006339; } div.document { background-color: #dad3bd; } div.sphinxsidebar h3,h4,h5,li,a { color: #127c56 !important; } div.related { color: #dad3bd !important; background-color: #00744a; } div.related a { color: #dad3bd !important; } ol li { color: Black !important; } ul.simple li { color: Black !important; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/.static/request-lifecycle.png0000664000175000017500000034526700000000000023432 0ustar00tseavertseaver00000000000000‰PNG  IHDR 6¨ˆìZsBIT|dˆ pHYs † †æ²4tEXtSoftwarewww.inkscape.org›î< IDATxœìÝw|ÕÞðgf¶¤'B@è½÷P¤)ÒD½ " Øõ½*`C®¹®Ë¥ErAQ)é„N hH¤m™9ï³3;³%†ÀïëgÍNݳ˖yæ”áÒÒ !„B!Äøê.!„B!äÖaPîDGç\u…êÄpà8¼<ãÁAž§»¡ËK!„Rs0Æpñb!MpµÆ"·29€ÈSjç¼Ü'„B!ä<‚¢&X„B!„Ÿ¡B!„Bñ „B!„Ÿ¡B!„Bñ „B!„Ÿ¡B!„Bñ „B!„Ÿ¡B!„Bñ „B!„Ÿ¡B!„Bñ „B!„Ÿ¡B!„Bñ „B!„Ÿ¡B!„Bñ „B!„Ÿ1Tw!„k‘’’‚ 6 °°³fͪîâB)—–VÀ :&Ǫ»<„T€€Ç9þ‚—çq<8Ç2Ý}ÕX^Bnmû÷ïÇ?ü€Õ«W#55ÀqDQ¬æ’]»•+Wâ—_~Á´iÓдiÓê.!„T Æ€ôô+*X"Š"N:Wæ:µk×Bddmuº¸¸.üUæ6±±1°ÛEdddV¤ejÔ¨Ξ=Íû‰ˆÏsÈÌÌöºÇq EDD-ð|Õ·bcŒáôé4O=Ôãg`³ÙP·nÄÄD¡s綨];¬Ê“rë*,,Dbbb™ë ::ÑÑш‰‰Add¤JçÙÀqùòåj-Ãõ`³Ù0uêT\¾|ÇaÁ‚×í±8€={öx]Þ­[7têÔÉmþâÅ‹QRR1bêÕ«WáýŽ7µk×v›¿gÏìØ±©©©8}ú4‚ƒƒƒ¸¸8ôë×]»v…ÁP±FW®\AJJ Ž;†ÔÔT¤¦¦"$$Í›7G‹-ТE 4iÒf³ÙmÛ €±²OÆ>øàƒ¨PY!žUèÓœ›[€¶m†”¹ÎóÏOÁœ¿¨NïܹwXæ6kÖ~ŒŒL<:õ¥Š£LmDÇC¯y?¯Íú?„†ãùçÞ,w]AYÇaâCcpï½#àïïwM¿}û̘>ûö%{\Îó<Ú·o‰G{?|ï5=!„@nn.žxâ‰JmÓ»wo<þøã3f L&Óu*™wÉÉÉ$ 'NÄöíÛ}þø×‹ÕjUƒUnnîu}¬7bæÌ™^—¿ñÆnäܹs˜4i’:——‡—^Òÿ†—µß„„]9uêf̘իW—YÖàà`¬^½ ðºNAA^{í5|öÙgåÖ„ ‚€† bݺuhÞ¼¹:ÿ‰'ž(7€Œ5Š!רB$44 ¾3g/àßsæÁf³«Ë† €aâ{÷ºmÚµk‰/¿z‡çŸ.Ö-{øá{Ñ£gGtìØ7:k?n»­'Ú´q~lÜø»®æ¥_¿îh×®¥:½~ý¯8sƽ֣n½:¸çng9sæ<Ö¯ÿUމ‰Â˜1wªÓ§NÃÆ¿«ÓwÞy‚ƒqâø¼ÿþº}Oœ8‚ àìÙó8tè22²‘‘…¤¤}˜1}V¬ü ýû÷p+SE¼õÖ'xãõÕéÐÐ`Ü1´?Ú¶m£G#)i.œÿ þ‰ŸÞA„R%bccqþüydgg㡇Br²óÈ£>Š~ýú!##GŽÁ·ß~‹ÒÒR$%%!)) _ý56mÚTá³Ó•qäȈ¢ˆ:¸-kРÀß߿ʷ:¢^½z¸xñ"š5kv]ëé§ŸF§NðÁà—_~Qç?ùä“xæ™gÔ×XkݺunÓ®ä™gžAŸ>}ðÓO?aîܹ€—_~#GŽDË–ÎßðóçÏ£gÏžÈÉÉÔ©S#GŽÄÈ‘#áçç‡ýû÷cáÂ…8~ü8®\¹‚¼¼<¯Ï%11Ó¦MÃ¥K—Ôy½{÷FBB:tè€Æ#%%û÷ïÇ—_~‰’’œ>}………ºý¤§§#;;/¾ø"6nܨÎïÕ«>ýôSDFF"::Úk9!S¡_ “ɈƬún=RRN©Ë¦Ïx={ºWÑFE…câÄ1ÀDà³Ï¾$Iê²ÿ|0î?‹ÿ11QêôÅ¿.éÈè»ïÀO<¨N·jÝ?öŠÛ~ž}öaüóŸ“Õé5?nÖ¸¸z˜ûçÙ™ÂÂb„×n§žõhÜ8Ç>ýt1JK-êºï½ÿ ÂÂBÔí^œ1_|± —W€Ü÷$vîúÕw+WY6nüo¾ñ‘:ý¯×ŸÃ´iÂhtþåç_FLt—›¢3!äÆÁóéÓ§ãý÷ßW§ñá‡â‘GÑ­×¥KL˜0~~~x÷Ýw=î+&&111èÞ½».€téÒÅc&„\fÞ  ]ø¨ˆ–-=A6mÒ°Ò[?¦RÛ(Û}:ïMôêÕY—““×^›[é}½òò»jŠ­ƒ—_~R> ,,ññîg¤!ÄWš5k†ÿüç?ºyóçÏGiii•>έ<š•RK ­-¸žn»í6] ÖÏ?ÿìq½¢¢"üþûïºy’$aÆ ×ߺu+¹öÀµ™Þ¾}û°wï^uz„ ºð¡hݺu™eß±c‡îý‚ýû÷»…­víÚ•¹OBÈõwÃÆVz›–-›xœß¤iå÷ÕÊ˾*â©§ô}]’§TjûÂÂb;vR ñºnë6×·JžBÊ3bÄÝ¥(Š8vìX•íË–-UÖ¯Ãb±àï¿ÿ¾ªm%I™3gpåÊ•Jo›ŸŸÌL÷VJJJ<Î×Rj>Ê«¹téŽ9‚âââJ—O+$$]»vU§Oœ8óçÝ›7ÿüóϰX,ˆŠŠÒ×Z…@úõëç¶L>9ÜxÒ¸qcøùyî[YTT„I“&éjȦOŸ^nÓµ¶mÛ–¹œrýUûu@†„í;šªÍš*£V­P$íü’$¡Q£úؾc I“F•Þ×ÇŸ¼ÌÌlÔ­[émã͵'Ož…Åb…Ù\±Ž™gÏž×}=z§NC59O?ýº¢iSïÏ‘1†eKÄŽûpøð1œ={ññqhÓ¦9îà.ôë×Ýãv99ùøé§ŸñãêM8u* ³gO軆@’$¼üÒ»øñÇMèÞ£#š6m„¼Ü|¯?ë_ÿDHHà¥ÿ «Õ¦[^·^4ž~ŠnÞGñýª 8|8ÉÉ)B›¶Í‘Ð?>ƒû·v»ˆ­[ÿÀ«7aûö=èÜ¥-¾þú=Ào¿í“O¼Š êbÄÈÁxòÉݶ'„\žç‡“''NvíÚ…'Ÿ|Òë6£F‹/Ê•|þùçX¼x±Û:o¼ñ<ˆ·ß~[7ÿÃ?ÔM÷èÑ=zxïkwùòeÌŸ?«W¯Æ¡C‡`µZQ«V-<ýôÓ˜9s&ÁûÚ999xã7””„”””””€ã8ÄÅÅ¡}ûöxá…лwoÛ¦¥¥áûï¿Ç÷ß?þø¡¡¡ÈÍÍE^^>ýôS¬]»‡†ÝnGxx8f̘çŸÇéöÓ²eKÔ¯_AAAnqôèQ¼ùæ›Ø°aƒzÐÎq5j„6mÚ uëÖ˜={¶×ççÍ Aƒ°k×.uúçŸv«EP‚Æ]wÝ…“'Oâ·ß~lÚ´ V«UJKJJ°{÷nr ‹+×öË—/ÇìÙ³Ýúñ<}ûöÁjµ¢Q#ýïÞܹsqúôiuº^½zøç?ÿYîsmÞ¼¹ZkR¿~åšKBªFµÈÈÚºá{+«k×öêýîÝ;^õ~5ª_é~ “ɨ›æy¾RÃó6oÞþþ~()q6a˜2yV~7ßíµéÓ§úôéæu_iiéxäá騶Mþâoذ>4¨‡ƒbïÞÃX´è;<ùäƒø÷;/©åÞ²% ï¾3Û¶íÑõ/±Zm())ÅĉÏáÇÕ›Èú è…ýû  @VÐd2""²6^|Éy²ví/8þ",+Àh4 ÿžj±Ùìx{ö'xçù°ÛE„„¡U«¦HM=Õ?ü«ø–&®Æ’o>@³fñä 4cúÛX·n r5A¨aÃXÀ7K~Àc½›ÍŽS§Î!/¯€!UÌõ¬{½zõкuk:tPç ‚€Ñ£Gëëׯøøx¬]»EEE¨S§n»í6Ì™3Ç­‰<÷ÜsºéY³fy ¯¾ú*æÍ›‡‚‚Ýü¼¼<¼ñưX,˜3gŽÇm—/_ŽgŸ}YYY€Áƒ£gÏžHNNƆ pîÜ9¬]»O>ù$Þ~ûm]@¸ýöÛÝš.Ùl6L›6 ,pë윓“ƒéÓ§C·ƒæV­Zyl~µzõjŒ3Œ1‚€ž={¢[·n8vìvíÚ…µk×bíÚµxë­·ÜBMy „7ßtŽþè@cj6lN:¥+W®`Û¶m4hºþ®]»`µZ¨«]Q¸†¸ .`èСX¹r%jÕªåözxòÇè¦ï½÷Þ J`2™ðÿ÷å®G¹~n˜&X5Ù¹³tÓ-[6uë¿Q“ÉèV+‘”´½zŽÆöíÞÇhwuæÌytî4 Û¶í†ÉdÄÒ¥ãÄÉß±{ÏZlÛ¾ AA`ŒáÓOëF÷úóÏÈÉÉG½XýÈEÅÅ2x¼>“'߇¬ìC˜:õ~ÝüÄÄpîÜN]húóØ/X¾b iÓF(,JÅú ‹Ôåwž‚Ù³?…Ý.⾌DúÅ=ض}ΜMR_“Žbì˜Ça·Ëá(?¿‡Cl¬¾ßŽŸ¿Þ|ã#<òÈ4ÝHm•í[D)›ÅbÁ_é¯ó”€¯¾ú ¿þú«nˆÒ¨¨(,_¾÷ÞëµoذaXºt©z`ùÎ;ï`Ù²eøç?ÿ‰Ï>ûÌ­ ÍgŸ}¦» 6Ìc¹cøê«¯Ð©S'<ýôÓxüñÇÝFrš7ožÇ뇬\¹÷ß¿>fÍš…M›6á_ÿú~øá¬\¹F£Ññú)üqÝö=zôÀ³Ï>«;x.**ÂÒ¥KÕeS§NET”þûè½÷ÞƒÝn×Í‹ŠŠRGRddd`âĉj_ÁgŸ}IIIøàƒ°iÓ&:tÈãkRQ=zôЪ-[¶èjæ÷ïߌŒ øùùaàÀ9r¤n{×ѱ´ý?ŒFýI:e¾kíÖ-[Э[7¬^½ºÜ¡pàðáú鯗» !äÆpÍäÇ’%ß—y«ÈIMöóÏ;tÓ;–ÝiΓÏ>›¨¨pݼ´´t ð ½ãAìܹ¿Ü}<ñø+¸|Y>Ë6eÊ?0vœóGºK—vè׿§:ýÞ»Ÿã¯‹òp…Ï<3 ûö¯Ã[o¾ Ûß3OÏž=‡Ð©S„‡;/~X'ZíDMñË/ú×A±v|VðáGîÕ•ûfÉØ´i :: ÌŸŸ|a¨  <öØxuÝ””SX°`)y”²}û×aßþuºš¦ ëÅ›o~ŒqõÔÚyß@©J‹/Ö}¯·hÑäþÚQ³þþûolÞ¼ÙmÇŽÃÞ½{ŽqãÆû–<öØc¨[Wßö±ÇÓݺtéâ±\Ç!##[¶lÁG}„yóæ!99aaÎï¯ÂÂB?~\·]NNžyæuºY³fnàGމ &¨ÓK—.Å–-[Ôé×_|ð.`„……᯿þÂæÍ›ñÁàóÏ?ÇÁƒuÍŒ222pñâE·çâÚùzëÖ­ºZ”îÝõ'­âããèñu©£Ñˆ¾}ûªÓ9998xð :­Œ 7ÖÕÒ¸¥&«ÿþ/ ?üðƒ[ÅéÓ§qÏ=÷ sçÎX³f×òfee¹õíiÒäêûrB|ëšÈœ9ó0ù‘éeÞnæ²o_²: /„„á•WŸªô~bëÇxlrÈM¤ú÷‡;‡NÄG=n¿bùOøõ×êôø îÃ&$8«Á‹ŠŠñÃêÿ•Y¦aÃ"õøoøc÷œ9›„»ïŠvíZ¢Aùà gÏNêpŰjÕ]Í w°ÿî»u0 xðÁ{ÔùW0}º³ò˜1wº]ıw‚¾Ú>ñ›Ê,oDdmlظ'OnÅÑ?Æ'Ÿ¾víZ¢}ߌ$CÈ­àÀxåçðçƒ_ýµn)Sô}¼-Zä¶Ÿ… &Mšäµ“qU q;¾pA_k={öl]Çð‡zÈã¾|Pß”sÆŒ•.OLLŒ[“$×òxâÚq}îܹ°Ùôýë^xá<ú裕n~¥Ð6¡ô£a)C[û¤­9sæŒ:¶ÿ‡·;wƪU«<^oäСC=z4ºté¢kÒ§ð4èAÆ =>Ž(Š(---óFÃÛâ[×@ž~ú!¼?÷Õ2oWûex#*(¸Œ¬¬\$%íÃÌWßGÿ~ãt_\óæ½…¸¸Ø«Úw¯^‘’ú¦M{Ôcö_~ÙÛúß« ŠM›·ê¦ýýý––®»5¨¯?«xòÄÙ2Ë3hPoµ_Œ¿¿–/ÿûö¯Ó=?m-Hnn>6oÒ—cÅŠµ(,,ÆÈQCtájß¾dää8ûoDD†»•×b± ¦ngyO–]Þ†q±4(A}¿=úèØ·|B™ÛB¼Û¿?Ö®]‹>ú>ø ºwï®^»A¼óÎ;èÙ³§n›îÝ»£}{gÿ¼5kÖè:Ûl6,Y²ÇaêÔ©×ý9¸»öÇP–žú,îµÉÉÉn! "\¯æím(­1cÆè:ÏïÙ³mÛ¶ÅÚµkÕy³fÍÂüùó+]…kQ® ò×_©µ!Ú2|øpÝúJHQúy}-C‡Ejj*fÏžàà`·åÀ Aƒ°oß>Ý|×Z2À=¤)æÏŸ€€€2o®B®¯kî„>fì0"Ôš>í훦¤i÷á¹váí93УÇÕw„ä”ÙoOÇÔGïÇË/¿‹ïVê‡7,))Å軦`íOÿÕõ9’¬oRСýå>VyôñÀwé®Þ¾tÙ >Pþï×+<¢¿jûÑ£úòþkÖð¯Yúk ¸º|¹YˆŽŽ,s=BHÕQú]¸=z4fÏžíu¨Ø)S¦à©§äÚ`‹Å‚eË–©Ó?ýô²²²0dÈŸ4›Ñ^$Ïc Gêk–½]éÚl6#,, ùùòɻݎ“'Ozí$íMxxxù+¹ˆ‰‰Áøñãu£‡8qwÝu €¹sçêBßÕhÓ¦ ¢££‘‘‘HJJBII Ö¯_ÆÚ¶m««­èÙ³'""" @ Ó§OǶmrÓÚÞ½{»j剟Ÿ^zé%<üðÃxå•WðßÿþW·ù;wö¸!äú¡£¹JZ¸ðý«ºfÉÕ1rºvÛˆQ#ÁÁƒªówìØ‹¢¢b@˜Í&u¨ÛÒR ^ùŒÏš¾=0~´@JJJñãêM˜ðàÝøï×+“ëV–€}ÇÃ!Cúê:ÍBn  À¤IåŸÐp¥tFWÂÆpøðaDDD`Ó¦MˆÅˆ#ªº¸•³Ù ‹Å¢ÎËÉÉñZ ’——§›öÔèzš:u*ŒY³fáÛo¿U›K’„¯¿þÉÉÉØºuëU÷«8p @¹VCiŠåiô±#F¨DE¬^½ºBý?”Q»âãã¢ÿM1b>ŒÖ­[ëšîýïÿÓõ»i×®.€TÕÅ+ !× Ã{˜4éDFtÀøñÏz\‰3ôC>Z­6œ™”P¡&³gΜñºìÎ;ïÔM»vÐ¥K·‹8¾øâ‹7MŸSBnf@n gÏ^ðúÅYTTŒ¢"gǹÎÛêjš¤oæôþ{ pêÔ9¯e³ÙñÛo»®½Ð4£a‰¢ˆW_y€{çsE÷îu*5õ>øà«2c÷îƒÈÏw¿€!䯥­ÉÎÎÆ™3gp×]w!&&Æë6®®;vx¾ÆPU™1c†îzK–,ñ¸Þ²eËtÓ³gÏö¸ÞõPXXˆ‚‚œ>}ÚmÙ!CtÓmÛ¶½¦Çr­<7¿R¸ö PáNÝžžÂut²Áƒ»­óúë¯ë28|ø0î¹ç]M!äÆCäRRRŠsçÒÝæ[,V<ùäLõª´þþ~øÏ¯éÖéÙ³}ôuº¨¨=ºÂ¢Eß!7Wî0)IΞ½€/¾X†–-àÍ7>ª²²¹½Ÿn˜]›ÍŽºõêàŽ;<ÿ™LF|¾`Ž.4½òò»˜2yΜ9¯±œœ|üòËŒ> }Æ  €!דöêמ¦+küøñº+£åw>×^8Þ~ûm|öÙgHKKßþyMgø=iÒ¤ fΜ©N;v ï½÷žn;wâË/¿T§ÇŽ‹{î¹¾ö矺ÍÓ£˜˜·Ê8p nÚ`0àöÛo÷º¾§¾<ÚZ‘²ìÛ·ÏmXdÆ–.]Š9sæ¨ózè!ŒãÞœ×ßßË–-Ó]äÇD‡°fÍ·>;åz2®BHÕªP't›ÍŽÍ›¶âì¹tœ=«¿`Òüùßàbúßèб5š4i¨ÎÏÍÍÇöm{|$Õíì­7?F·nЧowݶùÀù÷ßváïŒ,ìØ¾G·lÙÒ5 AtL èUn¹wîÜói±téjÝü£GcÞ¼%hP¿.z'tAíÚú2dgç!iÇ^>|LíØ­x÷ùèܹ-š5GÛ¶ž‡Ÿ¼ƒÝnÝ: y³F«Š‹éXõý\L—Û$ ,˜ƒNÚ¸mûÖìiHI9‰­[å³F—/bê”1uÊ‹ˆ‰‰B~þe”””ªëwéÒpåJNŸNS·Süþûh×®%BCƒuW÷Äh4`ܸá˜7ÏyæpÒCcËlãݳg'¼=g^›ù¾zÃÅ‹WañâU„¿¿23m~CCƒÕŽ•))§pâøÝ{ëôé4üþûˆˆ¨…¸¸X]“5BHÙ^|ñE\¼xQ7B¼ùæ›Ø½{7¢¢¢ðÖ[oUz¿®Ñ[´hQîÁéwÞ‰U«œÍHSRRðÔSO©£hÍš5 ]»vÅðǨë1Æðä“O¢ÿþ;v,¬V+,X Û|ñÅÈÉÉÁˆ#Ô³ç/¼ð, æÌ™«ÕŠ3f`×®]èÓ§RRR°|ùrÖüQ IDAT‚ã8Lž<ï¾û®nŸ«V­Â‘#GtMŠ ð /à¶ÛnðaÃPPP€… ê†Ò€>ú©©©3f êÕ«WækóÒK/áûï¿G¿~ýP·n]lܸQíóa4±bÅ •¹òôï߃v»ü½Ü§O„††z]¿~ýúèСƒÚ±<$$¤Â!èÌ™3 CÆ ѼysØív?~\mÊwß}w™×7éÒ¥ ’““ñòË/cÞ¼y`Œ!==£GË5ó±±±hÕª8ŽóxQC­—^z YYYøáý…o.\ˆÂÂBDFFbæÌ™¯YB©„´´––VÀ,Ö3Ìj;íñvñ¯½ @™·çŸŸ¢ÛfÃÆÅån³fí×nuèðÿÊÝ.<<ÌkYµ·¶m[”»¯õ¹m÷ý÷ ÊÝnì¸a*CEnoÏ™ÁêÖ«Sæãñ<Ïþqÿ(–’úk™û²XO±ùŸÏfaa!^÷3bä ¶is¢ºÍ_þ»Üç[T|¼Üç±ëusêÔ¶ =ÿýÖ³.]Úy}ìæÍ³?yåæaVÛiVT|¼Üò~ñå¿+ùïp†YmiÌj»Àlö¿˜ÍžÁìöLf·ç0»˜ÇDñ2ÅB&JÅL’,L’lL’$ºÑí¦¹qWægÊd2]õ¾wíÚ¥îçÃ?¬Ð6ï¿ÿ>ëÞ½; ‚ÀÚ·oÏ&OžÌ,XÀÎ;dž ⵬;vd’$±œœœ2ŸÓ¢E‹Ü÷رcìž{îa±±±ºucbbØÐ¡CÙÎ;=–·}ûö^çŽ;îP÷]Vy6nÜèõõø÷¿ÿÍúöíË¢¢¢<~¯ßwß}ìØ±cUö~èÕ«—ºÿ÷ß¿ÜõgΜ©®?lذ2×E‘Íž=› 4ˆùûû{|-Ìf32dˆ××ÛÛmëÖ­ìž{îaM›6e<Ï{ÜwãÆÙã?ξÿþ{VRRR©ÏvñâÅjÿ¼Òn5ñ&ŠSr—–VÀ :&òçÎÍfÇΞ;å)4¨«^5òò œœZæ6mÛ6w«}(..ÁÞ½Éeng4ЫWùãvïß……Åe®Ó®] Ôª¥?³“““ïv¡ Uáäɳط7—²• »]D|ãˆo€Ö­šUêºv»ˆãÇO#ùp Μ½€úõcдi#4oïöšgddáøqï oßnÖwûö=$3ºuëPáòÀ_/ápr Ž=?4mÚÍš5BƱºÇfŒaÛ¶=eì hÞ<¾’,äxpœã/xyǃs,ÓÝGõàCHMPRR¢^ 0==Ý­‰UYòóóa2™ÜšqùBnn.Μ9ƒøøxÔ®]»ü |$//©©©ÈÉÉA||<âãã¯zØÝêfµZqþüydff"++ !!!hÒ¤ bcc¯y(ù¢¢"=zgÏžExx8¢££sM×I!„\=Æ€ôtùºK „Üü(€r½Ì;Ó¦MÃ3Ï<ƒ?ü°º‹C!¤P!Ä BªÂæÍ›qàÀL›6 ‚ 77-Z´@QQN:UæèW„Bn^ÚBWB'„R%Ž?ŽaÆAE¬Y³_~ù%ž}öYdggãÕW_¥ðA!B!UäÀêp¥»wïFûöíÁÃàÁƒ1kÖ¬j.!„]„BH•=z´®–ƒ1†Q£FaÅŠeÉM!äÖB5 „Bª„ŸŸŽ9‚o¿ý§OŸÆÀ˼‚6!„[uB'uB'„B¹~´Ð© !„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!„B!Äg(€B!„B|†!ä–Æƒ(ŠÕ] B!ä–A„rËúøã‡öíÛcË–-Õ]B!ä–À¥¥0ˆŽÉDZê.!Õ„ àÁqŽ¿àåyαLwB5–—\«‚‚Ô«WÅÅųٌ;v sçÎÕ\2B!äæÃž~Õ€BnQ—.]RÃX,Œ7ùùùÕX*B!äæG„rKjÖ¬  ›wöìYLš4©šJD!„Ü(€BnYK–,ATT”nÞš5k0wîÜj*!„ró£B¹eÕ­[K—.Ïë¿ _zé%ìܹ³šJE!„ÜÜ(€BniÄÌ™3uóìv;î»ï>dggWS©!„›BÈ-oæÌ™ÁªU«ª©T„BHÍE„BÊѾ}{|üñÇnó'OžŒ“'OVC‰!„š‹!„TÀäÉ“1aÂݼ˗/cܸq(--­¦RB!5B© ùóç£U«Vºy‡ÆÓO?]M%"„Bj „RAøî»ï¨›ÿõ×_cÉ’%ÕT*B!¤f¡REc7ÜRõZ¶l‰Ï?ÿÜmþO<Ý»wWC‰®3ú*!„RÅ(€TP¥ö¹j)¡û '„\<ð¦Nª›W\\Œž={¢k×®øøã‘™™YM¥ÓÐ|ÔµŸ{·ïæœwc|ŸB¹™qii ¢crÀqt`z­çݾ²ÃUð €+kEåžÍ,sû›@ÀƒãÁËó8œc™î>„j,/©n¥¥¥èÕ«:äq¹Á`Àí·ßŽñãÇcÔ¨QnÃøú c¬Ìϵö;K»ž²]yÛB!Áž~JOë{ÞCY§+û¸e09¡¸ž¸ô°‰çý”]Ö[ãàƒ©¼Ó§O£K—.((((s½Œ;ãÇGß¾}¯ûgJcàÀy `s|ypàܾ (€B© ·t©x s™Ö܇üƒî}]Í4§l\ù0ru¡ƒóxß5d¸îº27çÁruŽ;†§žz [·n­Ð÷K\\ÆñãÇ£yóæ>(¡L[£¡à\¾—tßm…B!Uç– W8œwížÛO—³\mòä=¬T–ëÁ€|Àày¹z_ÓôJ™çí B;[·Ž—æ[7ÏÁ rmÎ;‡ÄÄD$&&âĉÚ¦k×®?~<î»ï>DFFV]a¼œó(óûGYÄ9§ÕZ”2jH!„ЏéHEò.34ØºÎ˜Þæ;6r›ça}·åºÖÎt+¥fN7ËsM‡ó¨C4ÜÛx;ç{[_™†Û¶ÞÔì0B„TÝ»w#11Ë—/GNNN¹ëFÜqÇ?~€pœÒÛѼ p¹ï $¼úW¼£„ã•ð ^ #<§† m ˆ².x¥†…Ó…ŽWjR<5å‚:Ï›ê"7^ñ0<´Î#UI9bwç«÷i^^V¬XÄÄDìܹ³Ìu•ÏZãÆ1aÂÜÿýˆW—kk8´D7–¶ÙÓ‡ œüvgÎÇÒ7'Uþ(Õ(Î^ËLýE!ä–sÊÖx¸Õv€A9R r¨€0”Z å¾H’¤† IÒ‡ ç4s¬¯Ôv0GèpÖˆHp.SÀ•3ŽJÙÕûrù9ÉA²€—JÁÁΛ ñf0Þ¤?ÀÔÈ3Mó*¥ ”4à¬Õ`Jà€º\ ê}¼8x9”8BŠ0tá„ã 8È÷áØ·ÓÖ€èÂIƒHõ†'€¸¾ÿ=çL|¢¬ëæøêýzúôiõú"§OŸöXù³)À`0Àh4¢W¯^¸ï¾û0jä(„„„Èá‚9j*´'@4o§´óiصkúôuë:÷ŒÓ ë« Àj-%Ô¸-£þ"„rË»¡ˆþËy&RéKá¼Ïtóµ’fžä˜†c™¨ It®¯l£¬+J’Z«!‰¢#€8jCM³$G¨$B°dÃlÍ„Ér ~Ö,mùà¤R’E ³€—¬à% &Ïç˜ÝË«ÀCÌ93$Î ‘7AäÌeÚŒ7Á&„ Ô ‹9 c$¬æH0ÎäJ+GÀpçͬxA·\ <σäí•@«ËåÐ"¯ÏWÂÇçAE 5)ˆTq žM`në’ªåij} çq}_عs'±bÅ äåå©Ïó¼@L&Ì~fø VX- 2ÇG×®]Õϸ¶?ˆR#²xÉblÚ¼ `@|£xôI胞={"88Øýy*M´89œèprNm-Iyש¿!„ÜünÈRV­‡Ú¼ÉåG“)µL_Û!9šII’¨ ’ä ÎûŽÚ I‚¨™'1çzFkŒ¥™0[.Áh¹³5 fk&ü¬™0ÙsÁ1ÑW/S98X…P”š"Qbˆ„Å…RS,Æ:°˜¢`5Ggˆp¹„WC†ZóÁkç9—É÷áE &úZej®Jß‡ê ž‚‡ó}ÍtïqçgÀµß©Ž*Fæ|Ϻ½¯•DN?Òœ/Y­V¬[·K—.ÅÆa·Ûa0`0`6›áç燀€#44µjÕBýúõÑ¿_tîÜ1ucœM§$QñÔ3Oáòå˺Ç18p FŽ‰Ð PýÛM­uN«çŒ8/¡ú€çÚôÔ¹kª!„›Í @¼ŽÌ÷æVîÁÃq&JŽÐàìï!J¢3hˆÎû¢è¢#dh·µ—¯è4‚ STtÁÅ'`²çWm“{Ž8Áñ×q_~–{¾‹š¿U÷Èv>—ýâq9 ®ø5Ea`sH†@]ÈÁQ³ÁAxg qÜÓ‚àP8e9Çãg Œ¦FFD€S×s{™|zPW=DW»æ|OJ"DQ‚(ŠjV±Æ©#HÕcΦCr(wÞäσAoÚP¢¨ŽÚ»ÜÜ\|÷ÝwX±b8³Ù !88aaa¨]»6ÂÃȈÄÅÅ¡YÓf¨[¯.Ìf3<ˆ÷ÿó>À€Zµj¡w¯Þعk§:*Wtt4^›ùBCB/“ç+­«µ ®EÛ]í‡âÚTË¥¼¬‘¶!„Ô7Tñ><m§qç™a%<8C“Þ$Q=»'ŠŽ&XŽƒ:ѱ½Pšƒ€Â^IEpñq–œ-£i”¼ Ìà3 ÞL/ȲjÐPþ:NVü ©„1 œ$‚1ì€hQoL¹_ÑŽG‘©. ÌMq% .4…Õ/V‚à8àRjIäiAF!E\ ¯ëKâÚ¼K ^kC\B|sPçû¢­Ís 6»¢Ý›Í‹ÅŠüü\\¾|¢(¹¹Í=¹VÊpü FHPx‡A`0a00ƒZÛ  (@õõ90†³gÎâ‡~Àúõë‘——§Ö~($2"ááá¨]»6j…ÕBHH"""ðíòoqààÀðaÃñûþÒÒR|þÅçØ»w/`ÐÀAxhâCºç¨öùptR×W\8ƒC™!‚•¿Ž[m ½é !¤ÆÑCõÄsg[e¶|¿ÿö;’““a2™àgöƒÙl†Ùl†Éd‚Éd‚ÑhÄ•+W°wß^ù„Œ  gž`ŒÁl6£OB5€¤¤¦Ðo«#b9Þ¢j“,íIMÓYõƒæ»@íÏôN[ËâbÄY{¨‰!„ÔdÕ@ô?`Ðt"w.WÎî2&wø–gÕæRŒi†<ì¢æ¯]„(ɵÌ^Š Ü}ËÝZWöC,ÚßF=Á Θ‚ÀƒcP¥B†¶}¾s~çk0¥ 2ÜìJ¹+ÿ `òp»L>5ëüÕ¬sjS'¯9Ï>H~ [!`½"ÿµÂ1tÀ(^Axñ!„.-A® dõ@NhwH¦¹£«`/0<;A0€çí ¹»ÀK <$‰9‚¯Ž˜%ÿQšh)Õ9õ½Í;ªûÀ®*¹…ћͫՊc)bï¾=°Ûmºu‰ïÙívää#¿ éÓѬY Ô« ø™ý I~0™œÿ>JMHu½W];‰wlßÚw€ÍfñcÇpâø äåç©áC©Å9xè DQÏó0 8—vð÷÷ÇöÛÕ]†……ɻքAaÛ¶mØ´yÐ;A%Ë•.ThšXzñÊÓ¾ÚÁhH_B©Ùn˜&X€>|¨M®˜û(UJs+Qr† ]ð°‹°ÛE˜/§ òÒFÔ*Ø ƒXì^ÓÁñà̵¿pp~µ4À=”Íñ?‘ÉÃ4«‚û×BÍËîlª£ /8pxÁÁ`o0Ë¡„ãc 8c àøÝ¶—€•怕æÊ5$8&"¼ø‹€e.BN@d„öC^X7‚»Áƒ(8jH Q€(ˆ0 Iâ!0ÆãÙòP¾ŒÈÿæœÚOææ« ÑÖ~ˆ¢ÜìÊjµbëÖßtá#22;w€Ñh¬ÎâÞš@ˆÇŽCzúE@Vv&X*Cóf-!Iò äQääϘo›aiK[!? ÷&®f?35j0 ÔZŠóiçqîÜ95ü'ôNÀþûQZZª mZ·Á€ÛvìÜ´óiH[–†˜˜ädçÀh4¢a£†ˆ«§>®R6×>#Ú ªË´Í´xçóQ:®ë¾&µ5"ºJM3.×ZB!7ŸO}>´áC™åK’«#T‰ŽÎ¹rà›¬ØÃn—Çùr ê^üa—Êû†æ7ˆ78C‡9Ìc“ªŠ‡ëIS¢›®‚=3 ’­¢µ6Ç>9No4ÌŒfð8Ž þà‚bÁÅÊM¶JsÀJs«cäØQ|ŇP˜‹³µF#7´ F 9|Dg%ƒA‚Aà!I$xy”&€1 J“2ùd'×ѵWŸKÍ!’£y Ýnlj“©¸pἺ¬c§Ž¨[/F·¾öouÐÖÚ/Ð ¦þU Õ›ë¼*YÇYKè}[×yšë¹N+ó$ç¨bŒ1F!44!¡!hס¢c¢qèàaØívdgg!((ˆ©+üæ¨ñÓÖ øª?ˆú8Žƒq¥ÿ˜æ>ÜÃ8Àl2ãÈ‘#0™L`Œ¡uëÖ˜2y &Ù'áÔ©S8yê$ìv;bccѳGOp‡ÜÜ\¤¤¤¨»ùlþg(..4hÐo¿õ¶îa\ß³ºÀ¦–]÷ù–4ŸyM,í>uUää˘ü¤Ï•)Oµâ5#5ý»…Bj ŸíA”ò£é)|(W—£XI’ÜÔJT‡>DQ‚Íf‡ér ê¥/Ó'€óüjƒ3…iúNhËäzx_}xå«ú`Â$;ìÀŠÔ} ?æ@Ìà£Ü'&°.¸Àº€h+Í+ͬòõ‚¬éh{鿯âløhä„tƒÑ`„Áh€ÁÀÃh7Àh” ‰$ƒƒAù· 0ò¿€ÒÔÂ1J/’r0ç¡ì5ð@AwvÚQRp¹»÷ìVç5kÑL Úþê¤ žnR9Ë}róTN8Sv½–_ SŽyÙYÙ`Œ¡Nt4mÖQu¢ÐªMK$:H¿??õâ‚Á  !¾|r§?hgÈæ ]½ø Ë÷ÆŽ¤êvýûö—›’ hÓ¦ Ú´iãö’v&Am%ðhÕ²L&þ<ö§Üy — .ãÔ©SÈ/ÈGƒ Ð0®¡Z“ç1,A®õä ¡$IßtKSÓ£r|Jœ5#ÚÑ´´5+¨©iß)„RSù$€¼uS IDATh¢Ê ¢(ªÍ• ¡œ)¶Ùì°‹ì6;L©ˆ¹°a—å¡#uµ1àcäÎØÚr8þWå¡Ã5pšû®áúp×üðUWK¢­v[ ,W² Í0˜ƒ`07˜Á.0\`´Ü‰½è¢ÜL @-m3>Á•œú8[{4òºÂ`0A4Ê5XÑà¸Æ…%é++CLà}7ƒV‹Χ£A\}´lÕ™™Y€¢¢"! þþ°Yý`7Ù*ôáÑ«nD)pn#P)Í“´_‚AÀ¨Q£0jä(dçdÃdÒr ;@ç€siçžžhÔ¨‘>Ï100gϜłÏ¨µíÚµ“ûÐ;†/¾ü–R ìVv×  ¾nŽå- h?ÿºZÍçG»½¶ö]í ÏA÷YóÔçÌÓ|B!ׯçЙæÇÃ-|HŽNæš>6»»M®ýÌ܆Ø3ŸÀ 93¸Àzà¢t};˜æ±œs*[Ø«{ŽUKiG÷lQîobÕ…ÉnÅf¥0 ¿ ˜# ˜üåìaÍÀ•8‚HÀ‚­Ðñ¯ãdèݸ1¢d‚hdŽÑÌGÓ ÌhP ;ƒN’ä¦,SÏî2Ý‘¢ã™Õ¢=ÈQW°ÛíÈÍÍU—¹­[#\Íñÿ E #ú¹ÇÁl62þ¾„z±u£Ñ›Í›ÍŠâ’b”–”ÂbµÂf·A´‹`Fç` >P+ÇA9ÇœÜÊ3æàh–Å1D„G¨ó”¿®Cã&%%©ó´µÊ~–¯X®†Áƒc℉``X¾|9ÖoXÄe‰èÝ»7¼>-5$8j‘týY<4'ÓnçXQmÚåBÔÍþµÏU}ÝÊ C„B®Êu êŠK[\õ,°ã T’˜Zó¡„›M¾"´Íj‡h-Aä¹…ˆÊX#oÈ£Y7XºR\Cð¨iQ®¡¤ÌÁª{röÒ+°•\ÁsP æ ¹ãzhpAõÁ.Ÿ+ÍÇD4Ëÿa–H­3¥æPH’ 3Ê5’|ø¢Åq8‘ƒè¬ÚÏËíêpœ5˜òÞE………()‘;ðšÌ&µ]Y½Úù4ˆ\Kí‡7úý˜L&]“¥¢¢b„„#4,ÙYÙ$¥¥¥(µ”ÂjµÀn³9ú­Ižv~]y«Qú‡hk tµ!ÚfbÊ7sÿLŠ’ˆ]ì‚€^={Aõ¹Úl6?q\ÝæçŸÆŸþ‰„Þ ð—‡(Š8uúÚµmW¡ç¤í/¢kšMðP“¬m‚Ås¼óçÁÃÛDy-Ô0ãzÎFs…÷šÜô“Bn4¾©aΦWrð;š3e¨]Qžg³ÛÍ­œW…FaâŽÏAPÑqeW€1|XsÀà§}ˆÊš8Ê;تP…+o… ³[Š`/-„`ò‡9(Fÿ`¹FªV  (ìÊ9€Iˆ*9Œà ³p8ê ”5“›Þ™%ÇygG©´e7˜z-Æ”fWL×D}F5¨ÄÙKBQq‘º,4$¤JÃjµ"ãï €ãРA}ëXJ-¸téxA@ll=ë#;+F£Ñ1Ñ`Œáüù `Œ!&&ºæ‡%/”·¡Á`€ÑhÔ•/*,BHH0BBB• °X,ŽÚ»zATgÓRV­&à¬=pôãPkµòpïÜÍ8¦û>í"nr;vìØ:uê ((ȹ dçdË}e Í›5Gxx8öí߇•ß­Ô§"ñ…E…‚‚‚ÔÚÍœeTB‰&\q—B´A˱­Rû¡¼&ºNë.û‡äX.A#„BªÄu Þš‘(UÜJÓ+I’/,¨t8—›]Éáܵ±'߇Ñ~ÙÙê(0\pCuT«[#x\¥rŸgÕ¼¢µŹià fø×Š…Á wV7CÊ? Ø‹á/f£[ÆÛH »™áCÔ÷€îØBs6–3pà9‰sŒn#qàyÔ†5!tx"œJ`’¨Îãµ;®Ÿ›];ÿ@Òö€–­Z`؈;Õe¿ÿ¶ûöì´ï؃‡ Bff>ýd>žÇœwg{|ü ÒñÅ‚¯€Y¯Ïô¸ÎéSgðÍ’¥ˆˆÇ´ÏÃjµâ³O?ðÂŒçP«V˜~ƒëQ!áÕµ=c ÿÛ¸hØ0]ºv ŸÅÿßÆÍ°Z¬èØ©#Z·n©ÛNt\C¾DÇ…$ív;D»]>\¯Ëá3.Ãu™Ûêœf]íÙ~MÍ€ÉdÂÈ#1räH”––ªÁʱ!êDÕŸŸJKKˆ'%%%Ø»o/’’’’šÆê7¨ì¬l,N\Œ„Þ èØ±#L“®\›7oÆšµkСCÜ;î^Ô©«.sÑJ; ±}³7¦ù‚Ñé«½à¡²Ž¶H÷^Ö<¦F¼¼Ž„B*Îg£`)µò´æl°¤Œ $9›^YmüûÄžú8&É¿!¼AnâãW[ݯT™àq«„oÊmšåi…Ê艶RåeÀ/4f¿¹¦*¢Ü$«øxfGë¼DÙ/átÔÇã€B9dr\ „sÜ q$p‚ †ÖšÞKi~(IR¹/oaa!2³äNϹIyè? ?`·Û±í÷í(,’kQ®\–/„„>½ÁWw8»û…p‡Ûcí?áð¡dÄÆÖCè:øc×d^ÊDdd$Z¶l^¡}ÉM•¯S¤ @õÕÌyz\¯D^èÖÙši>—®M”üÌ~Α±ƒ€`Æ 8œ|«\»FÝ…¾}û¢oß¾ÈÉÉÁŸþ‰°Ð0¬]»ÄÁƒà€îÝ»càÀhøÿì½w˜Çyîû«“vf6/€E\ì" r`@I€£Ä ’0%Ê¥+[ò•eYÇ÷Êò=–,Û ×¶ä ã#SV EJ”D1GD r$ò.â‚ÀîbsÞ™é®óGu÷ôÌÎ qÞ禧»ººº¦¶»Þú¾÷ûÆŽT¨_˲ؼy3“&Nâ×OÿšP0ÄôiÓ¹æšk2Ú—^™ÂÓsØØ^Û‡"#ž=›|ù ‹Û7>w/ÿu3"uå Iyä‘ÇûÂEODèZ=lÛò’ ¦RnXReý¶ladÝ¿¤É‡Y€V|è*JËû²z\ìIÐ%<ñÎÀ=ëìߤÂ,@ã ôõ`¥„#q„¦! «!Pˆl¯$c»^£O/âxémÎKÜÉ"@h‰‚¥¡  M“Ž+–;ÁS-.½”‘±îF;Ãs]áóÆ Y¼äz¶mÝNwO·ÿT°,‹·ßZEýÑzFMeåˆAe’É$o®x›† TU#V;£v>|„íÛvÐÞÖNqqsçÏ¥¼¬ €U+WÓÝÝܹ³)--aÏî½=r”©Ó¦2bÄpŽ>¾}µ VÁ•Ó¦²-uµèîVdª¨¨™3gPäX[Nœh`ïî}DcQFɶm;ˆÇcÌ›?—T*ÅŽíïÒÐЀm+7±©WNEÓr‰áÇ1}Æ4¶oÛÁÛo­bîü9ìß·Ó4¹ñ#7 ëúY/ÔóÌöÈÇ¡ÿÈF.!55ÁÖ„–áºä_d¨¡BÕfgðúË=pÿ´µµ±víZ~û»ßòÊ«¯0¾j<ÑXÃ0øÜg?ÀÈQ#™2e {öì¡·¯—7ßz“êñÕŒ;Žýµûillô.óÄ/Ÿð¶W­^ŮݻøÌfÐ=g„èõksD±òÄ ‹ˆo¨ØÒv¥’“ )¾’¶žH[ªÐÅçöÍ#<òøÐà‚ÿ‹;ÛúB¹Ÿ8+UJ´é$L¦0:2zïß¡I'_·f¢•LÍMd埴]BÄãrDÎ>jZ|jf%Ì(Ò“ÖT2Awg+á‚8†PI!­d×&·?M¿^J[ñµŠ€Ð@sˆ¦iM­PB¨h66¡T¡—:Ù éð d³èSbìØ1´4·°vÍ:®»~kV½C(d¤ ¼»c§W®»»›Õ«Ö k·ÝñQžxüI¯L]m¡phPý?ù¯ŸRWw€Úýµ9Ëdã5kyá¹Ñ4Šaìß_ËÆ ›xøSË;n,Í'›Ù¾}Ñh×.¸†uëÖS_ ÛÑ‘lݺ];w³ä#‹ضu;­-­Äâ1:Ú;8Pw€íÛÞåSŸþ#âñ8-Í-lÙ²•H$Âúuèïï§j|Éd’_>þmmmD "êp îwÞ}ÇíŸ;oGÔÓÖÖÆ›o¼…®ëÌ™;‡òŠrOœ|:·2?ùðùÀ"™ù-¾É²iá‘ _ý´üå<×$™IB²-)B„.øÓ/ü)7ÝxÏ=ÿ{öîáÝ*qã KnðÊÎ3—9³çÐÜÒÌ;ï¼Ãú ë™?>ï¼óŽW®¦¦†/~á‹$R ¾û½ïÒÔÔÄ[o¿Å Kn`üøñ´µµQPPà…*¤î©r~ø#ˆùû.Û*’a "}ÝëÛ'Œ¿<Oyä‘G 5º»B˜a ±l•ïò±“ FíÿG +-Î%“=òqF.Wñ¸8¦ r•œ/ÌôÎÓ]Sbˆ@tФKJ›ž®vÂ1Á0":’ÝÈ~•3djëOYœ@B SÄCÓÐDJ}êš&°r‚%Ð °¥D÷V]=)ÐeÏMç}4^×u®Yp5/¾ð2/½ð2ï?ÎÂE èïë?åy'N4xäã~ˆÉWLâñŸÿ’ŽŽN¯ÌÁƒ‡<òñ¹Ï†Q£FñÓÿþyF™l$ ^yéUUï'—S]3žõë6òܳÏóÚ+oð™Ï}šªê*¶oßÁ‘£G™7.Çß;AÀ49zä(€÷YU5€[?ºŒ‚‚R©ÝÝÝüöéghllbßÞZæÍŸã]»··—9sg3sÖ lÛfû¶´µµQV^ÆýŸ¸)%¿øÙ?~‚ƒ1~|Õ}zõ5óyéÅWÐ4‚‚æÎ“³ìÁ¿²þA!Çò[;²]«¼í¡Îw§"üRÊ´eEH&MšÄ¤I“°RÇŽÓÕÙÅðaÃYUÊJË”®äö;R’L&Y·n ~›Ï~æ³”–•°ôæ¥üü? ¶¶Ö# ?ÿÅÏÙñîæÏŸÏƒŸxÐÇû¯ãw¹Êê×+kgÝ«–ÙRJ/»¼Hwþ¨c9]´.ÃgTyä‘Ç…Æ…# ά5—öÃv"aY–“é<¥ô…õOî9¤Ê">aƼsÓ¸ÔÈÇYàRsÕ:+kˆ@ fd¨ èëíB× tÃDÕ ›{!Õ‡)û˜ØúKvþ„¤.Ðu ]×ÐRº®ciMWãEs„ÛBêƒD¾—“–êïâÌÁü«æñÚ«oðöÛ«Bpí‚kXñú›§<§©Q%: ƒ\9m*B¦Ï˜ÆîÝ{¼2 ªLaaœj'û•Ó¦z¤$g½M'I¥”ˆþé_ÿFùÞ;îG®”;ñ?vô˜²|Øó¯šËºµë9þÞqººº‰DÂTTTpèàa¶nÙFcc£S·ê›ÎÎL"d˜×.¸ÆBŸt42ímí<öãÿ$ N6’€ì޵Ǖ z{{9xð 5ÕÕé§ù;õG6óë?.EøÃÚº.D:†lýƒë~DzríýÍiiM„g-™–ÝÐ=jtFÝþú³­ÛwlW°€Šò FV¦#´uttxÛýýýH)éëëcËÖ-¤R)¶lÞÂÔ)SY¿a=…ñBfϞͬ™³¼ëœÎ2’«]ÒÊÔ‰d„2ök}Dºr¹heôëåóxÊ#<ò¸ 8ïÄ¿4ØË Ák©Ð»Ò–^âA­¯™Šú'ÜSÁb•ÕœìipŽü%÷ÎÏšµ\jdãTÔÎ!/4D¨¡T§¤·«ƒha BÓÅ“Íï‚´Ù¿‰c]ïÒ£Í ©躅®[9è–ÀÖ4l!¶a#äÚôB$aÖ¬lذ‰I“&R欟 ¦©¬‡*Tl’@ @___F™@@•èðDþ½½½§¬×MÐpõÕWaLoÒå«xaœ’’Z[[Ù°~#áp˜¹sç°öõ¬Z¹€±ãÆ"„```€7^_AÿKo¹‰áÆñÆorôHý ]Eqqq†FýǒÒ&NœàMü¤””––0jkë8vì= “¯˜ÄŽí;Xùö**GŒ  g¾Œ‡ZöZ}É´|ärË’Ö%#bpÞ³~¨g¿ÿ²‰ŽsþêÕ«½ïÑX:oÊJ±ió&ïXuu5X¿a=)'2YwO7ÿñ£ÿðÊÄb1€äºÖ þȱßO42ô2BföÚ8ÛE+ƒÈ‘&€ƒž]—ñØÊ#<ò8[\Ä(X™îX¶eaY’ø‰×Ðìu‚ÐÅ}ç{[ç½m?y£wø\¾DCÅ1“k'Ç™W»DÝ|Ϋ±lx{r㩈 ®if•Èq ÍD ¦g›¦†„m[ôv·S/AˆFv`L÷ vLEO&I:)ð,,KwH¤îdª–àc_®nXg‹Å7,¦°¨)S®8mY€ÑcFa©TŠŸ‰ÉWLfÝÚõeÆŽ‹&4úxù¥W7n,›6n9e½¥¥%—ÓÖÚJgW *-OýÑzš›<ëÄøšñ´nheÿþZ&N˜@Qq……qêêꨪ(+Gÿº®Q9bÝ==*Ÿ‰ßd9kÆ6fìvíÜM[[%%Å”——ÓÕÕEmm†™=žúúúذn#B,¼–+¦L¦ùäIŽ?Á›+ÞâÖÛn9£þ½Ü0Èbè·€@N¬ì êî~7ŸFÆyrh àkøõôö°mû6@…ü­­­å¹gŸcê´©¬\¹’ãÇPZRʤɓˆ Â2vÌX–,^V¬XÁ5W_ƒeY¬Z½Šºº:,Ë¢ªªŠ9sæPZR:¨m9Û,|÷ã¾·È´ŠžVMÚiçY„´LËÈPÙÛóº‘<òÈãÆ œ ÝÛPIç!îN,-K"m‹¢æ^I*¡bíÛ§šßž>ÒÚâxk"Ç‘>ÖíëäÎù¥‹qË­Ëxþ¹xçu¬]»ž+®˜L£ãšåÖyÃKxýµ7xëÍ·B0yò$Z[[‡¬WÓ4îÿÄ=üò‰_±aýF6¬Ûˆ;½ò‡°­?ŽMÔêõ˜±cExvlßáWm(cìØ19r„Çþë§D"aÊÊÊ8~üÄiïqâÄ ï8Û·íà÷Ï<—qltŽdŒ†a°fÕZ3v S¯œ‚”’ÜxOþòW9r”={ö2ù Cñ^®ª×GL<÷,ͱØiâ!„Ò<M "ƒQñ[dæ¾ì²ë֭󬋯_LCC¿zúWðtæ)÷Þw/†nÐt²‰ýµû‡Ã|é‹_¢bXÁõ×_Oÿ@?ßúö·¨­­õÎ]µz¿|ò—,½y)÷Ý{_F8ï\ÚŒpÊÏ âö‡ëR%d:©aºB§­,5‡ÌeX›\’—Õ'yä‘Gȸà7dªg‘.I‡¯Ô»ê=’>©@%žÊ|%\x¦‡— £õõ…†! Çõæ,}ÝŠ€ ™ˆ` ²¿e½Ûh.q,6–-}yl5Òð²»cj¨0«(˜7o.ÕÕã G"9/¹ázæÎ›MÜɤ^^VÆçÿ¯Ïf˜…]·€ñãÇqâD••#(.)æÄñhZz@Þ¼ôF&_1‰¦Æ&FE´ Jcc£gA0M“G?÷$’x\é²FÍ—¿ò%:Lkk¦iR^QNEE¹Wï„ 5<üÈ””•©ý×/¾Žé3¦!€ÂÂBo¢÷ÀC÷S_ŒÎŽNFEÿÝ]ÝP5¾Šûî¿Ãüغ~ñu̘9¦Æ&úû((ˆP1lÁ` crÉÜy³™3w%¥i7¶x<Î=÷~œÞž^Ì×øC˜EÔ³­ÙÄBh™D_H1¨®Œð¶¾k¹YÕýdÄ¿â/¥dõšÕÞ÷E ‹ÅøÑÿú{÷íÔø{èÁ‡XxíBðÎÚw¼ëÜzË­ 6Ìk‡©›<ù»'=òF™vå4::;ؽ{7Ï¿ð<–eñЃQ_¨æÊ ±½[NàDèËvió ÷½ç·ìº½kæõ"yä‘LJ„€!2ü¶3µ j¿íŠÑmI°çHæù¦3ÙºÀÖlLfBe˜¹51Öíë¢gÀ"eK’–$`¨·AÊ’¼¸¹•߯o¡g@‰pu _YÄCו¦ßÛõð/7ÐÙgah‚%Ób ç7µ!¿hcËMvÖ÷ñíß*w“óÑÙj2ùâ–N_ÝHþÇ]å\9J…Fíì³ùÕÚÖìë%i©ŽˆwÏ‹róô4§s4&ùßovr¼-åz¿!„¤<¦óÍ{‹ØX×ÏßîöÚ»éP’Oþg ¹ÿª0·Lª«A⨰0çÖñ¶e‘J&0Ì€ ÛÛß@4Ù@£C>lÛVZ!7RšTãÆ–6‚tÆpµJyéº`577óÔSOqÿý÷SZzz½F.yy0r¡¼¢œrß„? 2¾zü r#Gd䨴¨7W™1cF3Æg1UeÑÓ_]5HlmÕ5ÕŒBŒmš&ãÆMï·%……qâñXNÑö¨Q#‘#+‘R‰P\\ì•+(( ‰ )ö.,,$çl‡¦iÌ€·=rÔÈ Kš‹ââbŠ‹Šð‡Wý0 Ãú1„E1C'’caį5Éø}¤oŸ\¾©©‰ºº:‚#Fxcóë_ÿ:õGU˜ä‰'‡½úÖ¬YãÕ±`Á¯]BzûzycÅÞuýì£Ìš5 Û²ùî÷¿ËÎ;yéå—øè­¥¨hðßVN½ˆk¹Èê¿æÑ–*±¨ë’•³>×’”m8B/2¸qƒwå‘Gy\ޏày@¼}Hï…oÛjE[:K³¯Ñ)ƒ—lÐûžµ•ëëùFC{‚Þ„"SFG<òJ3âZGF–)¼{¤‡7v´s¢u€¯ß7oMð½ßÇr^8…:¯oïÄÔÏí bÙð¿?ÉÑæBH®¨ Л9™äñÕtõÛÜ;_…¢üÑ4v¤ˆ‡5¦Tš¤$œhKq¬5…mƒ® ÂA߀j£®áÝ«©£²Ïã-p^»Ü¶Uߢ§sM„S'±, iK_nETÕXñ–K=«Ú¥J<\”••ñòË/óçþç,[¶ŒåË—³lÙ²»Ø8#Áó¹! ë†oìä1N76¾„„~-GÖÏuªÈtþœ#ñXœO?òiÞ^ù6sçÌõÈŠ¦iŒ;ÆsÝsë?xø 'N(×¼ 5¨(w\d5UçÁC=w®©S§2{Öl$]ש(K»ÓÖÖÕ2oî<¤”¼úÚ«lܸ‘¦“M >œ‰&²xñbÊJËœKË! ——7ÅqÓòˆˆë긲ùõ4~r잓ӥͅ_#’׋ä‘G¸À¡ÂS:Y #Ù’>t&Q•.þý%%tlhO %Ô ó©†{Ç5°â]E>fòµ©Õâg74óÄÊ“ì>Ö˦ÝÌ«ŽòÛu-ùx`Q·Í)âxë_âØ9µñ­ÝÝmV/×û®Žsûì(øþó-l;ÜÏ [z¸ùʱ° µ[Môo˜æcs#ŽœCÒÔ™"d L2§ÊäÑ+_ÿYcM¾xS)ÂŒ*±8™¿Û`¼ÿ‰bš€¤ë°ÝîXÄllÛ;¶²‚¸«Ñj¹oÞKÛúáâ¶Ûnã…^à¹çžã¹çž£¸¸˜»ï¾›»îº‹á#†¾‚Îé0tÃq׊|œ·š?ÜdHÿI:;—u E†û’3AEBÜxÓÜxÓØ–íT%ïlåÙõ IDATÖ?vÌX¾ú_eÕªULŸ>}¨¾««Ë»þˆá#2HPCc:°›3ä?ü7môö·¶¶²{÷nž{þ9î¾ënîºó®ŒûÏéjæè:Ü…6?ùH„í£Ë}H+-rÏèOg[Û…-<òÈãrÄEKD(P²é>9e:ª‘e¤ ÚɋդA8Ñ–£ sk¢Œ(NO’4ö{Ò‡öîß{F‘‰Þ~Ë+s¤±ŸyÕQŽœpê,™GY`ÒÈ;Ÿ:Äé©p°1Ý?»Ž P× ÚÜÜ¥Ú`Ù’ú–SG›ÌbmmÏlêaåž>F—Œ+ÓY49ˆžÃ›J ˆ#Ìðµ2iÂú~ëM¹Ý57·òÃ<†¦kèšæ%(š@Jç’+6†kÇY¿™ý3©!¶=_oÿþ¡ÑÒÒ’ñ½­­Ç{ŒÇ{Œh4ÊÞOuÍ`w¨?Xxš"N1ÆÎˆèºŽa79^ÞêqA0¤;RöŸ…Lg JøîBÓÓ.–®&ÂÄ£¶uCgæÌ™Ìœ9Ó³*øë®©©ñêkimñ,[·le÷žÝ€r7v›¶lÊ K/!ròäIÖoXÏæÍ›\ýM*lÇÕ8;dvŸe?¿¤&!^€MKÍt×òŸŸGyäq¹àÂÿdÙ¼9ÜÃ;.¥d à[ ¶ƒN¿XxtéÂß­mæhóO®>I8¨³tV1Ý}i¢q¨©ŸCMƒ³P·t+ëDo¿z…LAAPǽ“’è©»ÌÌ™3xû­•\}õUCÁÓœyžá ™÷÷×ÞÕÕÅ{ïgâÄ Þ>]×1tÃLçyÇ…‡ß +Á :8xŸßâž3¨.gHxx™cB/|Z磢¼‚–ÜÀŠ7W°uëV¾÷ýïQTTÄÖ­[½ëÝ|ÓÍ„ÃaÞx=­¹éÆ›øäßô¾_wÝu45¥£ÅÕÖÕ²yófššš9r$S¦LáŠÉépØÙzï¾|äÈ/nû³ó±xuù»ÌÎtùäâæ-îeÖ“Gyäq©â™ã3Ë£GÁ@¨Ò·O‚•Pî9™Œ. 2¡2̸Š_ûé!)›'V6qõ¤…ƒÑeéÉÙÝW—qÿBWüë¾,TbE€Š"“Öî$} ›#'W{ßLZ‚F:dwš`ì96¸ìÈ“T"¹¯ÝQƴѧ¤Ä²Aw^Êà u>u]Œ‡E9z2É®÷<¹¶›” «ö pÛì8z8DqY¤ì3iíO#„†a8V¥d:ÿJ—^惂Á `@m ‚Á¦ibè:š®a\tâŽ÷I@6oÞ @QQ÷Þ{/Ë—/gÖ¬YôôöP_ô# o¼¾‚ÊÊÞ÷·ß\ÉÌY3/>„3Y‘H„Õ+WS9b…EEèºîœ™·x\,œ.‘_Æïõ'™+ìmî‹ÖS8ã¯Añg^÷_ãSŸü†a°bÅ vìØ‘Qå„ ¸û®»8ZÔÛ÷Ýw{õ Ó§M÷Ž=ûì³üú7¿N·{#üî™ß1cú >÷èç¼(t^ûÜ&eõ…[wvÈc¿Å‹Bæ'1YÁrjk|+’Wyä‘Ç¥„‹æ‚åŸT¡œ÷ÕÄú"Õ ˜%’J‹ {±1ÝâbDq€L/â¥-­ $mžßØÊC×W0id˜ŠB“¦Ž$¯lm¥$f0ut=ý)5öóúö6½y8ÕÃC\5!ÊÞcÊÕêß^jࣳ‹x÷h/ mƒsŽTÞ½®ÞÓMEÜ`Ë¡^4.{õ„0ÏmîIJá‰5Ü57ʸr“–n‹=ÇXµ·—ú#EŒþíÕv¦ 0ºÄ  $(Œh躆“0KÑÂqHÂÁ} ÉÁ“)^ØÞOAP0k¬Ac:Û·íàÆ›>’wµºຠeÈmQÒ»\7-ÿ9¹êÊÐ?8$ÄO>¤TnZŸüä'¹eÙ-lظC‡a&L`Éâ%^.€ÈéVvèà!|èšÎí·ßN8æùžgûŽíüðß~È_}í¯2êʾo¿›X6IÎ&)Ù±ÒnŸ™e†rëÊ¢šåÂû)›Gyäq.¸ $#£K8HO.B­i-Å‹Ñô{do#"65õ¹l]DÜ>¿”×··‘´$¯nkãöù¥ÄÃ:_¸¥’ÿÿ÷Çèì³ø¯×²Î’ž(úÆE¬Ý×Åþã}¼×’à?_k"И<2ÌÞ÷ú2Î*Œè\=¡€µµ=töYü÷[- Á•cBì<ši]j²|Q!¯î ¾%É_ió]^bø¢lí<–`ý~‚4Æ …)*LJën®à­½ô'áµ]€dtIñÐÙ$.š® +á§ìo[¹«5iãèÓˈ®»ã„cÝ ‹˜ønø¼¶ï|£´´”'Ÿ|P/vhê B6¬ßÈS¯ L[;~ðÏÿʧ>ýIb±(+Þx M47·°{çnÆŒà YÂÈ‘ÊR900Àï~û{öïÝOqq1×-^ÄÛo­âó_ø,ÏüöYÆUe×ÎÝ>|„—‚²²R^}åuj÷×’+¯œÊu‹¡ë:–móÚ+¯sèà!úúúˆÇ ¹ûãw‹Å8tè07l¢áD†i0{ö,.\€¦MŸÆc?þ K–,ö´y|°ÈÂÖçž•},›8d×å‡dè%äКÿ9Ã*¸í¶Û×\Ù9Ç t‘•*C§<ðÀìÝ·—¦¦&žøåìÛ¿éÓ¦£:×_w=I]]WýÎ];ùÊW¿Â„š Œ«çí?xð S¦LáØ{Çø»oÿ‰+‰°àÚÜzË­”——ê‹\÷892 Gv7¿0(â—­³É>Ïÿ=<òÈãbâ‚쇟'*4‡t‘žlöGÆÒX|ÃÚ”(Pvƒ`‘ŠÊt®V¡ƒf€fªÜZ:øUS ‡<íªÉ%9÷GL¸õšôyRZ¨Ø±)ØH S¤øèܤ´O{º&¸uvdÌ­Ô zÛþ÷OE¡Á]óbê˜Ðš†£1@À”*˜2ήôՉɕ“+uß‹ÎWæ<Âq•‹‰lßï ЛÅ‘à\Bš†a¸ãAS¯4áÇ*"]Ë™nyx“:)Lœ²o¹úš«NYߨѣ¸vá5,Y²˜oþϿòlLÓdǶ<ðÐ'¨9iÛÌ7‡ãÇO  59¬žPÍ´éW"¥ääÉf<ÄŸþÙˆEcØRóú«o°ä†%$°¥M"‘ ‹3z´²~¦R)‰ýX–E  ²rDÆ«°°––<¹Ôq*½Hö»Bp ®Ãuu*ß”÷é#0~â/‹Åøëÿ÷¯yêWO±æ5lÞ¼™Í›7³pÁB_·˲èéQZµªª*nYv kÖ¬áÝïR[Wë]·°¨¤Ò…¸äcÔ¨Q444ðÚë¯ñæ[orçwzÚ“¡úÂ}®e»\åêÇ íˆ¿˜È<j¡/»?Ý2Þ"y¬<òÈãâá¢h@¼ ¤ãn…m{“KM躆® ŽŽxˆâÎM¬dëDÅ,ÐhB`g‹O51Ö Ðƒ Qñ…¼? 4 վɾ×^[¹Ùp‚P$mÙ= Úi[ÎÍ8+{/¿Û‘æX4‡d(t:C¹Ÿ`\:îIÁP3¨’Ê®zä€Ê¥"ÑXg~Ý01t]70 Ýš{sM8‚ó´ Ev¶á#„k!zŸ}‘L&üCß=W¤¡§»‡@0@*•bÔ¨QhB 5±cÇ  Ã4•Ÿ¼®SS]M DJ›žînt]çÉ'~•Á}#‘Á@yóçÑÝÝÃOò3â……Ì™3‡Y³g¢ë:wÜy;«V®æ•—_eòäIÌ¿z^FVyÝб,‹<./œN³íb”>8øœ\y9$2ãá…þvÜ‘ü.QÅ%Å|þóŸçŽ;î`ÇŽœh8Áu‹®T¸ÞšêöíßG{{;óæÍãÚk®¥½½µëÖ²æ5>|˜ªqUX)5'N˜È7þúôôôðâK/ò‹/°s×Î 2¤hßGœh:ÙÄ•S¯9l¨¨cBŠô+Á9.í™Õ]¢ã[ÌÉПˆl/H!š¦£igwß#F §ùd3“&M<å5r!‰`ï½÷……*òϱcï *çF¥Aa¼ ŽÆ$†tý÷—˜P(IJ[–rÓÍ7ràÀžûý Äã1ªkª©©©¦ºz<í¼³v-/<ÿr¹wö¶vJKr[*Ï'>\#ìƒÅzÈù¬òGÊ}û]¯¼Y½³x!³¬!*++©¬¬ôÎw?ï¿ÿ~¾ý÷ߦ­­¿ù›¿áᇦº¦šeË–±lÙ2N40|øp0b„Š,·¿v?ßýÞwYþÐrîùø=Ìš9‹öµðÒÝÝÍ¿þû¿²k×.B¡“'OfÑÂEÌ37CÈ.¥äÕW_å•W_¡¨¨ˆ/þé™ŒÆÆ&ÊÊJÑtêšj I&“ ÐÓÝCqqñÂ8&ÔpôH½WoWW7ÉdŠ’Ò O@òøà0È:r íÇ©ÂÔªƒÝ¿ÜòC¹tMš8‰?ûÒŸñÓŸþ”£õGùÖß} Ã02-ùþ÷¾ïMä?þ±“H$xíõר¾c;»vïâî»îæÎ;îôÃÓ¿yš;wÐ××ÇÖ­[Ùºu+•••<ò©G2r‹44¨@' 6Á@ÞÞ^~öóŸñéOšåË—sÝ¢ëxì¿ãÀüúé_ Y¶t ‚&T¯æ«ñUêÔñì³Ï²¿v??ùïŸðømmm|é‹_¢¥¥…ÚºZ =ÍÇ©r¸.ª€'ØwÿÎ=a{ŽgI¶+à%õÌìê?ügnyäqáq- .ñP¯pWØìJ4¡¡ëËÖÔ„Ó´±,SX–Åá3 PÓñ{e)HvcŸÜ†(¬F„ËT˜V;©àB;/ç …ÓO`.‘)NÎf¼Ÿ¶ Bá`X}•²ã ²/½Ú¾Ÿ™l0ïA † L qþé†ãŠe iÊ%KÓ4OóáášAüW¿ÌÌ!é{ÈÝ¿BåÊtžï«rd%•#+ywÇNfÌTÉÖ¾óýðŽìžÁBÙï~ÿ½Uâ`(È}ÂûþÆë+2DàŸýÜg<÷*¥e¥|æÑ?¦¯·Ÿžžbñ†®#¥ä¦›oäúÅ×ÑÙÕIaa!BhH[RXçÏþï/ÒÙÙ @A´À»¦”’­[¶rûí·—>9[²‘mì2—†Ô9äÚz‘¡êÏÜé¸'¹õ;õ˜†É 7ÜÀ 7Ü ŽÙÒ ™»~ÃzV¯YÍÁC¹çã÷0oî<¾òç_á ò$’»v²téR@¹19z„·Þ~‹¥K—2sÆL^xñ…ŒPÝ¿|ê—lÞ¼™yóæñà'Lß–K¢N1ܲû+Ãâá®9øÞ™ƒ"lektNáž•Gyäq6¸H"tõ@wE³Âу(,Ûè¶aHÛÆ2MlÛ¦¾ìc´™5Loù_í.5¡m߉D\‰þD¢´Ò«(W—0¹˜xß”fÐ{åÌ;RbÂéõd7vû~H©<&–4X-oå`àZ'»¹ú˜˜SÇÔutñ‚NT,]óÜõÔËOM½À—á Ñ›¬±j®3ÁÒ[nfÿ¾ÚÓÌdzsç.ÊËË9tè;wîæžû>~Fç†Â!‚¡ §qaš&%%%¹È<ÇuuñO’:::™pˆ­L×${ý-ˆ@\…•Â@ØIÇ¢_Æž9¹áƒ jÇ4CA "‰b˜ ÈžŠ(:‰;e /òÁ+ †1 …‚‚&Á`€@À  `LLÓÀt\±t]i„4MYÊülrn/FZžà]¸û„@8Ç2¶ßgw|[–E*™¤«»‡ƒ*ÚΘ±cB`èF:læB<£ªjÜYk&­­m477ǹyéTWWŸÏæe"K…BÞ¤î\«>Óž®¨('‰ðÞ±÷<«Œ®›Äb1¢Ñ(Ñ‚(¡Pˆ`0ˆišzü¤íü![»‘½?ƒ˜dw»æîNÿ.~—¤ì:ü“ñœäÒy~°ðÚ…Lœ8‘®®.éïW‹/#+GòègEÓ4Ö¬YäI“X¼x1Ñh”ºº:ÚÚÚX³f 555 «†‚uëÖ±~Ãz,XÀ¼¹ó@ÂsÏ=ÇÏ~ñ3^ãuªÇW{¹EܾÈõ<ôò}d}ÏØ/ðt%RJ/+½7~sY›?°yä‘ÇyFgg¸@A+'žË~Ú ¢i΃‰&%†­! e‘¶ã°e«UÏ$l¬øKªÛŸfBÏk€„TvóvD¨ fì$"‘Í@êa†×ŇÜ"’!_gfõH[<Òz¤ì;‰ìyϳz°¯`µv¢ŠxLƒP(ˆ0+ˆA À0\ˆ É«ëJˆž3›Ëúqù¼3&/B‹FÑu ˲èìì<#׊KÑh‹®[v‡b°Õ"KKIq.­(ˆªhní*L¸-í´FISI4óDãâb@Ý·/§›VZš˜Yƒëɰ*¸“u|“õ¬òe ™6m‰Gªd«UUUèºÎ¦Í›øÏÿýŸìÝ»—{ï¹—eK—1{öl¾üç_ƶmV¬XÁôiÊ%rõ;«½ú.\ˆ le³fí@EÒz{ÕÛüè?„&4jjj¸ÿ¾û)/LH\kH¶•$;ŸŠD¦Iˆë²åë+¤× óÈ#<ÎÅ ðL¾êŸë‚%@ê »+/ ¥D8ϸLsúq?m¦uâa!{›ñ°^Ù$6ØaŸy¦©¬€AÐ ¨í á¹`LÃ40Lˆ@וû•® 'ˆîäÉ5Á»üV˜•¨^é;bÑííH)I $<÷¤Ëé~.U¼«¡  9Ñ»º»»5IÓ5ÝG”}QÊ.S÷ÀËïK/’QÀ·-ÿnƒ~Ǭ“«þ@0@Í„šŒ²áP˜ÂÂBV®ZÉÚuk¹rê•X–å‘™¢¢"¤”´µ·±k×.ÊÊʘ4aؽk7íííÞu×­[G$¡³³““Í'9tèßþÖ· é¦ús£¸ĤÏʵþøî{õÕÕˆøwç‡vyäqޏ(™Ð=׉·Z¨’îéHi¡!<I—n‘v‹i×çòfp*•]o1©ïU¶“´p ]%¸ ÄÑQˆ`‘rÍJõBªW=ˆt3…_VDäpÊEÖ›7tÃPI #óEl§½ Èž`'½ÝIì´¯b§¾;THÐ4ÉpˆF0`bLLç»i(b(¢2¡ëY¢s—Ä^~Ö?ÒV@x¼ÈËP_Œš ÕØ¶®_Øä™çÉúqžN?Ûj„T¯à½÷ŽûVÈQãÕp­u>÷@g‚—<. ÓéEÎô÷ÈÐTdé@\׬Œò¹¬("=ÉŸ:u*ßýÇïòü ÏóÆŠ7غm«WÞ4M–-]†¦i¬]»Ö;ÿÚk®õ"Q­^“¶ŠLŸ>¯|å+ßþ‡o³wï^8zô¨">§è¯Ò×>ß}ºoŸ&2Ïó•Í#<ò8[\4 ˆÂè>¼4Í}¨ƒ¡“Ö#d ß„D ‘hNhÖãÚÍÔG2ªs%“û_',!щlÝ4 ‘áˆP‰§K R½ŠôR ªä…ð‡EHNÏ+È= ¤“•\Ï)ÙìkFö6â†Õ—xÌg§XD*XˆiŠ|˜Š\‚.Ñ0  Ì€AÀ `˜†G>LŸ]ó¹^¹„cpÈØËËúá÷#W„Û`Øðá­? À¾½û¨9‚H$’ŽáŸÇû™÷CD4McÔèQDcQسkwL× ¥÷0LÓû›ñ“æüDíƒE.­ìýÇ|ij-þßÔýžËM ȘäûvR-à¾ûîã®;¶–ƒ‡põUW{zŽ5kÖx§,\¸Û¶I$lܸ€p$Ì£Ÿ}Ô#6Ñ‚¨7èý÷¸k×.^}íUšN61zÔhî½÷^å¢åw­’>×*__øu/Ù.gê5‘Ûyä‘ǹágBCÔK]…Lû¡Z€.eºEîÄSm*ÿj-9KÓɤÆ{E7SŸ\ȨžU\Ñÿ:a©Ä¡${•X=TŠ—€pÈH‚>h&RŠŒh8œþ IDAT~°— ÎLºA®©—;öCêÙ“^ ‰nÏÕ k ãh “öU¼+‘ b:!Ó%*Á iªW`0 †éäÉ"ºÐÑu×Jéz•-<¿Üà¾Ôu]e~¯^IYi9Í-*Tñúµ¸~ÉuhšÒ†œÄƒ .G͇*­Æ˜FA´€Q£G ‡R²}ÛR)X!™LÆ•k–«aÒ«]—Þ·^dˆçj.ˈ»?#Q¢C ÜÜ~F `ê•S™:ujFÝõõõ­OkG*G¨Ìì[¶na``)%óæÍS9A€D"Á»;ßÀÐ ÆVE"Yµr?~ìÇ^Xßúúz¶lÝÂßûï9räS§LUá€Oóh‘R"ì´P?Ûm+<òÈãlqñ4 vÅR+)>7¡#,0ð‰­ˆÀ™Œ '2’pòEè$ucÆÍ,bLÏJ&$Ö³›œ $2щdçAÄ”p=Tª´!Òkob­ ¤f(ë —ˆ;ÌYÍC3Í!Ùpú5÷äV"Ðß‚ìoÍÐv¸è•Qö1“]b!É@!†a(âá¸Q¥‰‡‰p> ]%4 tépÿ¹äCèÂô¦W“s“Ëer(­€P¢û€É“§°vý;¤RIz{{ykÅÛÌ¿zÑh4#/Ày¯ªéjrNN!®:žÞNW«¬è¸ô•Ì “šUEFy¯Lf1@YѰ÷›õôô°mëvO|nY6º¦…ˆD"„Ba‚Á †ibzƸÍãÒĩܱ-œeÿŒ9,#9­"Y“t7·ˆ¿NOøí´¥²²’¿ü‹¿dõšÕLž<Ù#.®û•‚…×.ôÊoÞ²™0sÆLf€ŽŽ~úóŸzÏ7ºÖ†øÑ~ľýû0M“¥K—òÀ'ÈÑ9é¶úݱ<Ӣȼÿ<òÈ#³Á' ™sé‘×Ê iÛ–ÊÝǶ±¤šîKÃYòÜVRj•^¤Ðt--5 ŒdRs)‡’!š¨gTÿ&Æ&·¥%Ý D2Ñ…ì< f,3†Ä²>6…ð<Œ„CJt¤Ð•ÕDh€³í9/+B§z¦Ÿaý®o†I(7¶!+Nö"“]ªÚ3t.úd„Cò Šé4éÕhºšp…]í†a8$뛺“`Ð%$fšp¸et ÃpÂìjš† »ëX?r†¾¼Œ‘vÁÒ1tå†/,dBõöî߃”’¾¾>Þ~s%UUã3n ‘HäÒÒd»d@š˜\ŽÈ&%º:U8ÕÚýuÞdNJ•|®0VH´ J$R@(IJ,¦é$Ðt£^ž¥N碕óo.‡e$×3ÊŸiݸ{®Kî_¤ž\:3fÌ`úôé^‰D‚C‡PZZÊ”)S<}Ñ;kßñ®åFËzó­7)ÆÏ×ÿŸ¯#„`Μ9üó¿ü³Wg$ÉhkÎçlÙÈÖ½ä‘Gyœ-.R"B—„øÍÝRÍZl M³±íLw,ÃYíšr‡Pÿw!]CKj蚆aéFФ¡>S©¦a0`V±/8š]©;ˆ'Ž0&±…1Öv¢²-ݰd72éD´Ѓ3ª¬$fT…öŽS°’9¦"MH„†é<YœÁ#)NB¨ÜƇ¬Ú¥³Ó]aK'dÎ÷ åÓþ€5 î;Ñ¥>“ÝÊ”2ÌA9™ƒL£AŸ€¦+âÔ5'"–î ǽº†æäòði;t=-2w-ºáX=œ\zzå8›|\ÎÖ?üÃPá‡C¡£FƬ#™T§C‡sèÐa„ÄâÊÝ' T2Igg–eeì·RRJâ±8ñÂ8ñx!±XŒîînþäOþ„ªªñ<´ü!nºñ& ”+çå8F?¬J¼žë˜Úé;9ɶ¤øEÞîLN]QªÜÕ# òÃü-[¶L&½º;;;yw‡r¿ŠÅbÌœ9‰dÿþýÞ5ï»÷>Ïåyø°á¶m›ñUã9Ùt’XUœkUJ‚•Lߓ՟&\9\ªü Äa{2‡¸’ãúD„®¶!_r@ÃÙ6tE&LÓ@÷ȈãŠåÓÍ´¥Ã‹d8,M tÝq s4";òÕùH`P$Ͳ„Ca,ËbÄð„9DGg‡wž”’ΎΰånH)I¥,L#@¤ B<§¨°ˆÂ¢Bb±(±•¼ð‹}:³gÏFÁÊ•+ù÷ÿwöìÙÃm·ÝÆC=t^2·çqñq6z‘¡r‘ ñÂÚºå2މÁÑ¥†ºþ‚ ¨ªªbõêÕ\uÕU^¹Ñ£G³wß^„lݶ•›oº™ÞÞ^üqµ^&%sfÏ!•J‘L&I&“´µµ±{÷n~÷ÌïV1Œ3g0kÖ,L×-ÙµÞç­yä‘Çy‚8r¤C Ñ‚~"“ë!î†â°mÛ[¥±¥Ä¶”{–”ÊllY6–mc[)ËÆJYXζmÙX©)ç{*•RÇRVÊV„Är¶mËvŽÙ¶%ÖÅ©£”Y‡)³P.¡ëìoÖsÍòéF„†ÈúŽ“ãéZSlïŸÌúî?ËIgB†hd$r Mb4ÍÚ’"â„UÑ™„ó©H‡æå8ð[?4Çú¡~kˆ²thºæ$-tÎsHЧÛñ"š)-„ër…C@ðéU.ù(õ‘óûàºÒ©ßJ8Ç2¶Ï287¾mËJ‘L¦è§·¯Þžº»»ééé¡»§›ÞÞ^úûûI$¤’IR–¥ô6Žïxž|œ/Hüú)áŒuÓ  ‰D"D"D ˆF£Î÷ápĉ‚¥gDÁBÐßßϳÏ>ËO!½™ëœŒLëÙõeW+ÓÇs Þ[[[ùê×¾J?£FŽ¢­­îžnõìðo|Û²éìꤣ£ƒÖÖVZZZhnn¦¹¹™–Ö’É$W]uK—.eΜ9ƒô yä‘GïR±cj^}Ñ ˆjÀ©Iˆ‚&eZøiÛi;äÁ™ˆ¹„D‘ E.9±9±mµm)’‘²RªŒí#"–:׭Ͻ–eYD¬J¬z 좲•»mDiÃ`°PûR€N·ŒÓI1ÝÑE1]”ЦUÒ!*T‚?Ý ¿«y„CÓ„ q¬ (x®R†²RèÙP$CÓ†¦¶=²â#ЏGX®êZn­Gú_î—Ü…}ñ]|8cÎ"™L2 ¿¿Ÿ¾Þ>úûûèë¯Ÿ~o¥2•Jyn]yœø]l\«œáOƒƒ!"á0áH„`0H8& 1œDnÉ\cµ¹¹™§žzŠßüæ7”••ñàƒrë­·•½8ݘɩ÷¡rŒä´šøtÊ À-Œª×­GJG îÓ¤+÷éFiKÚÛÛ‰Çã?qœ'žx‚úcõD£QÊJ˼ćóæÎã‘G¡©±‰ãÇÓÖÖFk[«"--4·4ÓÚÚJ[{twwS^QÎ]wÝŽ÷ÞKMMf¢Ã<òÈ#3ÅN@T#NOB@:Ö<᜔i;«Ç¶-ˇÏ2bÙÊgÛ¶,¤í‡PX–k ‘NYÛ±²ØÞqÛ–ªÞ,Bâ‘"ç{Hv±ÚˆÑBÄn%&[)mÄè@')"‰Nê¼ô™N“&ºeL‘ QD,¡K”Ð-ŠèÕ ÂðDüšP95²'þnÞ—ˆèºæY'\w)Mw­!i7,]èšáMWZ]7<²¡{u 4@h-[ãAŽLçù€‹I@€ "¥t,uIÉ$‰D‚ÄÀ ɉ„cýH¥<¢lÛ*$®ß•#³C.w×’¡‚'¨|6Á`3 ªmÓt‚,˜ãøLÆj]]?þ8/½ô³gÏæ¡‡bÁ‚ì!sŒä7Cmçqf8U¢C?2µ$¤£qô…ã\¹{2iâ‘ãÚ?ûÙÏhkk㳟ù,á° %ÝÕÝÅ7¾ñ šNªÐô_þò—™;g.}}}“,¨eï»e9û\òb¥­y±¬ô¹nyÛ³°8¤D¢ÄÀ>ÒᑤGHܶ«¶Jü¤Êÿ’?ÝŠc®¨+ƒÈG×%/Ô«;[ì­ù,¾Ï4‰p„“¸ÐÕ†hŽ%DYL2ÉKF]ÂO8C‘u¾ûõù2gßû…ÅÅ' IBÒcZ¿T*¥\=+œíp|yœ?¸«ÒžÅÐsÇÒ}î…ºïoIdd>?—ñšL&yùå—yüñÇihhàcûŸøÄ'¨¨¨òœ––JKK‡<>h|ÙvÎ~‹³w^ÞÕÿ¼À¯åÐuÝs­òŸìÞ‚Òî\ƒ"l9?VWWO=õ+W­$•J9—UçÅcq¾öÕ¯QUU¤#l¹×q“"&SIÖ®]Ëïžù/¾ð"ÿ‡½7“£¼óú(6l`ݺu—ìZîÛ·¹­­ÄiƒA¸z¸âˆÛ©É­!åOëüø€ñ_Ä…S°„xn[¾È•’mþþÁ¸OŒàÏ °|xþ»®õC)±‚”ˆ²¨Âñž0³£xOÅ*­AáŠ_À¯Э– Wxઊ’bLH¹UD-ù‘ô\·”Â{÷-¼—¦–‰·–J08WUg:]®*Þ‘é P9°(Æ?æŽûöªwç‚!œ;Õ¸t¥º˜4¡RD_ÜïëÐÐÏ?ÿ<Ï=÷áp˜Ç{Œ/~ñ‹Äb±’vßýîwI§Óü»÷ïJÖ¿A7?Û&ç0±,³ð`Ƶúú÷WɬvIP({s-iº›1P+ÔF*¸¼ª…û«‡WŒÔÞï‹wÜ CCCìùhd3Yf4Íàþûî'‘HT|g=âËÛ®* ðóŸÿœgžy†M›6ù$ø›VÎüùóÙ°a6l`þüùçÂ,[±‚+Vð«_þòŠIä ÂÔ¹"ˆÛ±êï´†_W"%ˆb»ÿäØ.dÖ*#®˜(> ¤°åíïØ¾È(ºb„‰ãeè<‹‡?§0X´ý~z~ûîù(EuUmüâà›¼ÏÆ F, ·š|Qˆ¸ƒ/“”[?EEõ­… ?…§l¾[VÁÂQ|"牊  —Z²ÝÝOAÅ}Õ/Y]xLëáµ»üLŸñ¨&DÀsç+¢øñqÁ¸jÁOù÷µú2\úïë‰'xæ™gxñÅY¸p!6làž{îAUU~ðƒðÍo~“¯ýëüýßÿ=‘HÄ=…ÀwÇvl¿&RÞÌ3pvãûŽÓy´‹îãg83Pr¿.šªKÆ©ŸUÇŒë™ÙÞDª©Þ/Jj„ BF¨9P÷-&^°z¹EÄT¯FAH*N!€½ððÂ÷|8Åâ¶AÁCàí8GŽáÇ?þ1O?ý4œÒù®[·Ž 6ð裒J¥Îíb•ñ7û·üíßý 5µI^yé%fΜyAÇáòrÅ É„ˆ¢Ý™*„ˆïöTž@(*¶ƒoÙð] m¼'–]8N@x¸Ö'pl|×ßE¥à«_tËrŸ­ˆ2Áù%ÄϘâY;bÄÁ¢ž/»ŠâW”/¤{–ˆÂÞ2ÁQ ZWü}|Ñp±¢°‚å Þð×Užêtú|L¿É,ÁÑá!¾2ޝ:ªºúßM%Øôò_÷?üÿøÇ¼õÖ[Üyçœ={–'Ÿ|€µk×òóŸÿÜwÙò¨˜–I>—'=–æ­Þeç»Ý{ZNÅR™G¤píieë qC³—Íä–Ïß@m]›]-%r“†Qò¨\„? òû„ïVåT~w½×AW®  ™,åîæÍ›Ù¸q#Ï=÷ýýý“^ƒP(ľð6lØpÞ™ßÖÜx#™ô(ªª’ËåQu_¿ô2íííç|,A¦‡+^€Àărß‚P¸¡V>-VJ¬ îzÏE« Pnø’¢/~@ÈĊ㸿¾å£ÄÅŠb`°ã`;Dˆã¯ƒ ø äÇ ÄçºÚÇå…‡gxWÜ0DWH¨Å Zp]° ÇR Ãoã €«”¥´@ÅŽg]ñ­øÖŽÒ ò© ‰Ö_>® ¤úß@µ¡Œp¹˜þï©‹mÛ¼þúë<úè£ úëçÎË/~ñ V®\‰m»éÈs¹Çöç×?zþîÁÀQU%‰‡*Üx„‹eYŒ¦Ódó¥)Ü#±«?»’ö­Äâq¢Ñ(±hŒp8„¦¹ ¼ôª~>¹\Ž—^z‰7òÊ+¯øUØ'¢¾¾ž?üÃ?dÆ ¬]»vÒö}ü1÷?ðõ©$ªªbÛ¶[?)—çå—^bÅòåS:Ž ÓËU!@<¦$DJ^;eËJ@„ÄøY´Å l/f‡ ˆŽ@ ¹'Tü¹oáÀ·Œ8·*o~‹æo§`Æ)É”8W÷G H‰ðð+Ÿ•€µ£`qÝ¡\SªRþº¸Þ³t(+‰/8‚Öן«(ZJ¦ñ¬ò…‡Ç•'@ÆCâ=.=WÎ÷r|¾÷½ïñgögëÃá0ßÿþ÷ùÊW¾B.—ãÀžƒüìo^ͼ˜‡–Æ´4Ö2Œ«â\¯5LÓdhd”£äbdåýKYtãj âñÑh”p8ìºg>«ªŸÙ8f«qï^A!R"nǘì{Ò××ÇsÏ=ÇÆÙ²eË„m=.\È7¾ñ üq?8¾ÿÛ¿ý·üìgÏ»BÙQ¶iZ Œò³Ÿþ”Ûn»mJï)ÂôqU Ê›hñ±Ly°º·®š)qÛ*ÕðŠ*Aᡌ³oÑý«r9ðD)ت‹ÊýÌÀ*Ï¢WúAê‘¡ø‡QüX¯êxP`(*(Á|ô¾°(ˆ”à_ÀâýØUúÅO<ˆ»ò=W„_ýêW|ë[ß²¬’ø,€L&C6›åïþîïxð³òÿú1Ã}î ß0t–´Í#Vˆ¹¨¸÷y÷ÏŠ†ÞlümþöŠC:í&j?Q?&‹¹¨Ö'UUÑCº_°Ò²lŽž:Í™×I74n}ôšf5‘¬M’¨I¸–HØ Ñ´’˜ºjý) QáJ¶yn¼åûúVöê§9<ÈÆÙ¸q#‡ž´½¢(Ü~ûílذ¯}ík%æ–e1áBbÑpÉ÷Üs7M‹ÁAžüáøìg?{þá’sU É,"î¼ÚºñÄH`}á‡ÈþŠS%“ @¬‰ç§ïõÇ)vª¢ŸQ2V‘J1Rºt…òGÀšâ‰ _ÀöQUµrŸñD‡ÿÚëÎÕ$<{çS~2~ó2—,Ïìà®G™E¢4£<>#øº\`ݰÊÍô•ŸA5®|Ñ!×Þ Ì«ñqäã£þ=§©¾þŠ—gœyñR5‹Pð ]ô«¥€C&›%³pîö8ôë£eA‘H„p$B$Áhší»o•ß›KbÒ{z0ɉ¢*EADÀrRÅËÛ÷bÜËÃá0<ò<ò½½½<ûì³lܸ‘mÛ¶U^U%‹‘JÕE'탗%•ªãÿúÿ3gÎð—ßùÎ÷Y„KÇU+@‚œ»ñ}—J¶— os™q£*Êžd9%ëüõã¼Õ%¬rb6å÷Þâ9_* ÆY¯8à”Y5ÊDÌx‚£Ú¶jˆð„Ë‹¢“kxÅ»Žõ¶:45Ô»Kׂø(>3¹„Œ/AŠkŠîMçÓp4Œª©dsYB†A<% ‘ÍåëÏ0::J<'›ÍËG ›aÃ™Õ •? *=ÿœ¬hÁO„\*øö·¿Í·¿ým:::üx‘£G…XM%Lùâ*Šk³®KÖðý¿ÿ>½½½üõúOUݶA˜~®¹¿Ìrߨ‰¶{ËSa½V­ð^aÒÜzZɼ°¬«~[]×ÑÔbe[·z¸»MÓ54]E×Ut]s'­Ê¤k…6ja­X%×?ž^|?Móûån/T¿ºnɨñQ |j~•Æ{J_Íä\n%)ZJÊìq¢­ XPš¹h__‰÷óP(Ī•+ÑUí¼úçý.Æböíû„?÷¹)eàáòqMÄ€œÕnjÝЃ{îþUö«zŸœxP_µé9Ž*Oå<XJÎïÊûQ¡:^œšm{s{¼–ãc43Še[„ô‘P„3ƒgq‡¦ºX¶EÿðcÙQ’ñ$Éx²dÿ¡±aÀ!Š¢(*==DBaêkê+Þ+gæèî'›Ï‘Œ'©ÕøÛòfž±\ºjæ¨`;˶èî'M“ŒÕRØVl>K6ŸES5b¡cÙ1LÛ$Ž£( ¦e’Îf@qH„•×(;ÆÀÈ©DQ#Z±}8=ÌÐØ0#B]¼M)¡hÝÐÆÏ€¸íºÂÃÄ2M,Û*ËÜuíóÔSO¡ë?#­|§j¬X¾ûÕéS'¹ç¾ûx套hii¹ä}ar>µ¤Š’ nú»*U_?áÓ« L÷SfŠš"4áÚ ˜L£l>u^Üò2]}Ý,»˜‘A:ûº˜?³‡×}#]GùÝŽ7NøíÛZZùìM÷ »ñýæ),ÛfUû :ÄhÖu{Y0«ÏÝò za`¸óà.6¼…œéùæ;´µ´qÿ ÷ G8pê ¯íø]±c{å?ûƒoÒCtœ>ÀÛ{Þe,3æo›?³{Vß…¡­AuáÍÝoGyâž <÷îódóYî[}í-mì:º‡÷ì ±¶ž²öK%ûo?ð>»ŽîtMçÁ5Ð\× ÀPz˜·?~‡în¿}ĈpëÂ[hož€Ò Eª÷­6–eûâÃ-‚[™±ëZä“O>áÔéÓ4Ô×ùßkUU …B躈½,õ*P”‚K²RôJH§Óœ Àé¾N:ûº˜]?»dß\>GÇ鬜»‚X8†i™tœ>¸Ejߨó&Ý=DÃQ®Ÿ·šæd3™|†·÷¾KßHš®ŽŸ[õyÛ² I\‹–—åìZ?þǤ¦&áÿNÕÕÕ1³e&3fÌ ¡¡žúzoj ¾¾žoîmk(n›;g.7Ýx±X ›»ï½—>þxºOQ>õˆä˜ê~ÒŒóü=!B‹185tƒo}þO !¶íÝŽiY„ËZ— ** fϧgà ‡Nf,›&.º#-˜=ŸÏÞt?Ï¿ý'Μ¢ãd7/¾‘OŽí .žäK·?쾟f°ù“-:}˜l>KKª™æT8ðÖîwÈ峄Ca¾´îa4Ucÿ‰ýØŽC<c᬴5·Ò?ÜϾ“ܹj}…ë@m¬–šh‚áô}]ä¬\¡/uœîëÄvlß‚1»afÅþ\MÉDBÞ;ð>'Ξ o¤Ÿ³Ã½¬]x mMmX¶ÅÆ·ŸÁ²-Ÿ9ÊœÙsÏ+{pðóT>–pÓHÿã?þ#‘°A(¢µµ•p8\êf¥j(þܵz8¶[ ر×jd;˜¦IÖÊbY6mmm8p]×ùìƒò³Ÿþ”µk×N÷é § — ‚ \.‚.Xçܦ9¾ø/¶²ù?{çíFJHsª9°Üĉ3§s]·G‡üõÁ6Åc Ò\ç¾Þôñf><¼›âK·=ÌŒd#8ÅþŒfÇøùæ_–õÆaptˆúDªê¹]×8›½'ösº¿“œ™£&ZÃÂYóyïÀï9iYhªJKª4N@Ó4k,2é\€‘€[ZCm£Û^Õ¨‹×Ñ7ÒGÚÊŒëŠ;U.ô3½šxçÝw1ͼÁó§Exxüø™gÐT•xC÷P7¶âf"kŸ)ÏçÂ?ñ³gÍ*Š…xœx"áÎ ËñXÌñxÜÏ|%ÂÕƒAa\šêfpÏš»ygÏ»|xh7Úío«¯©«hßÚÜÊû7ÝêÞÍ©&n^|#ËZ—rv°—w²ëðžÂ3’Ë¡®Ã S—H²`æ|¥hBàÄ™SŒfF©ÕÐÞÒFØ£êC™!N÷žf$3Jm¼†ù3Û‰hQVµ¯ä÷;ß`eÛŠËxeA®}D€‚ “¢éª¦úþ‰3'YGïP//n}™X8ÊÌú™lÙ»Ž|Ì×=„ŠÊ‘¹ûmæÏn'Žrjà4µuµ,l\À±îãüzÛoXxÝb¡('zNÒB,œ³€™õ- §Gè •¨»¼\áFˆ ‚PM×Ð M×JG9gϲ¶öÖIW¯á‘»¿ Àì¦Ù|ر‹ÛW®CQT6¼…™3fòÕ{AFÆFøï¿øÌlžI$Åv,4MÅQl¾ù…?Ûáù·_`nó¸é>>ã¬ç¯üˆÃ=GXѾœŽ]¹iùܲôf‚ÕÉÇaÏáXÕ¾‚Û–¯-V@0!bá(½C½"@A.""@AUSÑCº[Ý{ ‘àÃc#dó9êj& /m]ì/ÏiºŽ×¶½N6—% qº·‹[–ÝäºN9‰X‚¦TŠª¦  ¡¨ ‹ZŠ„È›&]ÝÌi¹Ž#gŽù"¢¾®žž¡³¨šFKC3»í\×N}"å÷¡¥¾™¢© fÍ'U›*ï2Éx’þáþÉ/œ ‚0eD€‚ h††ÒÏ9Õl$FA!›ËNÚ¶6^ë/ºƒCÞ2 "ŸÏ‘ˆ&JÚ—¿˜Q×è/§³iÇapdˆLáý“4ÕÍ`í²[©¯­ç£#³õ“­Ìn˜Å}7ÝKm¬†Ý@2žäã#Ÿ°}ßû´Ô·pÏš»¨ X;2¹,‘P䜮‰ ‚01"@A>Åè!=d ¨ç—÷ÖÐ ’‰Zú‡û™=cÖy÷#™Hr¢ûKZ ŽcsêÌ)·.wŸD4®iÌlhá†ÅkJܨÀu³R…Es²hÎBÒÙ1^Üô2;:vr×õŸAQ̞ςÙóÉd3¼¼í×ìèØÉ=7Üíï?46H}Mýø— è‚ 猤áAø¢é‘D#:oñá1£n}C}tŒë®fßñýì:¸›‘^Ûþ:¶cO¸¢(¬^°ŠîâX×q?£VïP/gÏpðÔ!LËܘ]Õˆ†Ý,W‡O!oæWHiªN4\´v cÛ6 µó`*W[QÜóS&ˆ½A¸Z ˆ Â5ÄdƒVUS1ÂFIƪ eõÂU¼þÞïY¿úŽó0_¿p5£™Q6ïÞÂe+Ëæ-enóBFhÂýîXu;8ðów~IHá8†¦óÀÍ÷ÓPÛÀ‡wñÊÖWIÄdsfÖ·°fáõì9ò/o}•šhœl>GSjk®ñ}àÔÚf¶ GKÒ_.œÂÿ"DA¸ÖPŽtZfö¢(bG>­(€¨(JaŽê®ST”¶’e.ÞNÎÇqp˲ÈçsŒ¥ÓôíãÿûF¡k–¸ÁßzXG ]šçN?zù)n_µŽ…sœÛŽ%Ù§iåù»çÿ÷Üx«æ¯¬p­rçÅcX¶ÅàÈ!à Ž•´ËšYFÓc$ã5¨ŠZ²-—Ï1š¥&ZãV‹*f–*f‰*Ùæ”¶sª´+y]˜;Na ®à[‚‡u*V|›Š…ÂÖ²‚ˆþ\MÕPU ½à 78<ÂÁã'ü½C­*ÑDŒX,F$! £š¦ûâC„‡ W;"@A®vɪª¢ë:¡Pˆ™óZXpë<:¶ºÎœe`pˆÅíóˆ†Ã8y‹‰Ký]}(U^U[7ÞËËå fÙ6ÇNuÒÓW,ਦj[âÄ㮉Fc„#aBF¨à‚¥–ƃ‚ \¥ˆA¸pƒÐU?^  ‰DXu÷ ²™Ç?<…£8dr9víë ˆÆ¨‰Ç…Œi Á¸*œÁ.B'mÛf$ft,ÍðØ–eùÛÔ:H-ª¡¦¦–ššZ‰b±¡PÃ0ÐuM„‡ × "@A®rE)ÖúPU ÝÀ2L"á0‰x‚5÷¯$5§–½o$;’ÆÒÆÒ™’'ðÂ4 CxŽJí¬‰D‚d2I²6IMM]Óùúc_gýúõ<þøã,]ºÔ!"FA¸š‘Jè‚H%tájÇ‹‹°mÓ4Éçó¤ÓiÆÆFapp€þ¾~í8Æ`ç™,VöZsÀº:P4%ª ×h$® ¯ƨ­­%Y›$YçŠDM ‘p„Í›7óì³ÏÒÓÓÃW¿úU¾öµ¯‘J¥¦û4AΉ`%t ‚ˆ®v<â8Ž/Br¹™L†Ñ±QFGGfhxˆ‘‘ÆÆFct` Ó²°-Ûqpl«èm$? ‡B€»¢€Ñ0b^z]H$L4#“ˆ'¨©­¡&QC<'‹û™°4Mcdd„Ÿþô§üä'?¡±±‘ÇœxM“{‘ W>"@¡ ÂÕOÐ !Ùl–t&íZDFG%=–&“ÍËåÈçóX–åÖ qll»,Û”pAx…"QÜÚn,7Q@8& ‹ÆˆÅãÄãq"áÑh´"=è~uðàAžzê)^ýuÖ¯_ÏO<Áòå˧ùLAÆGˆ T D¸6(!¦iaš&Ùl–L6C6›u—3žøpˆ'Z\ñá”XT„óÇ xDEÕ4tMC7 B†A8!vë~DÂaÂáH þG1ûUðxŽãðÆoðÔSOqìØ1yä{ì1.û¹Ž‡|‡./$\©ˆ„ D€×å–˲ü¸wÊ‘Ëç± ëLËÂ)´u¼ýeÐxqQ\,¥P«ES54ݵ„ºŽa…ŒW:ºn`:ªªù‰ÜCL<°áùçŸçÙgŸ%‘HðøãóùÏÃ0.ÇUĆ‚¸ò]ˆ(®D€B"@„k‹ ñâB,ËÄ4-,Ër—- Ûr_;Žƒ]Å:ò›p1ð ºc@·b½¦º–MuÓ&놎®-š¦]·8÷äÑ£GyòÉ'yõÕWY»v-O<ñkÖ¬¹ÈgV¤\xTZÐ.[‘•O3Áüþ”41"L"@¡ µGpè‰Oø¢# P\׫Êý… #8àó¬ ŠB‰…#8yGUUpœ 0:ŽÃ;ï¼ÃSO=Åþýûùò—¿Ì7¾ñ š››/Æ©•|G¼ï™7׉¹¸×¸\¸§ "D„ËA¨@ˆpmR1@ÄÁ±ÇÆqð_CÁâá·—ÁÉÅÅñ†‡ËŠs866Æ /¼À3Ï<ƒa<þøã<ôÐC„Ã኶]]]ìß¿Ÿ;ï¼³úÙTA1ëέ’„¥ß/ïØÅÀ7w@A¬*ª‚ª…l¹°Uý½E„— ‚PáÚ¦š‹LðÉ´X;./å"ãr?>qâO?ý4¿úÕ¯¸á†xâ‰'¸õÖ[ýí™L†T*Å÷¾÷=žx≒}K¬8n ç@¼‘e™X–eÝû»T€¸ÇÁï…ã uÀ¸6וOA ¸õéº^!F@Dˆpù"ˆ>” êƒ@#—†© ô¦c0¸eËž|òIvïÞÍÃ?Ìã?N8¦©© €ï|ç;üÕ_ý•;Ð-s­² 1D¦ibšyòy“Á~zzºéíëÃ4ó¸ºÃ Qé7ÃZÈ‘L¦h¨¯'¹I4ÝϨf†ŸYÍ‹1šj‚A¸ˆ„ D€ŸNDlL/WÒÀ/“Éð‹_ü‚7ÒÝÝÍ|ào{ì±Çø‡øÂáp‰ø0M“\>ÇðÐÛÞÛÊ©S§Èå²þ~òýš"ѳg]Ç¢…Kˆ„#„Â…š3á†Â0Œ’¸#¸²¾‹Âµ‰A¨@ˆ ‚ǺuëØºukɺÛo¿^xúúz×âa™äs9:::غ}3Ù¬+<‚¢c¼LLÂÅ%ø$‹³lé2›ˆEcD£"‘¡pC7|kÈg%\z‚DŸæ¾‚ ‚pñÝï~·B|lÚ´‰¥K—ò /pã7’Ïçxó­78p°pÁŠ¢0gÎu4Îh$•J‰FdP[*Ï{‰6NÑ‘QéïëçÔÉSX–ÅØØ(;v~@[k;­óÚ0­„Ÿ$€°—•­40]."@AAÜú!###üå_þeIv+€|>OGG¿zéW,[¶Œý{KÄG2™äæ[oªšUëJç²»Šyá0)FQ‰‰D‚Ù³gÑ>¿wîb Û¶9rì0F8DóŒf ¹QTUUPW|hšæ‹HA¸Ôˆ – â‚%‚P`QKÓ4Éå²ôööñ‹F>ŸÇq.\À¢%‹Jö™Îøòš$6Mñxv0ÍõøÇÂ[æÂú…ãuˆÆb$qfÍžE"‘`ï'û8røáP˜%K–‘JÕS—L’HÔ‹ÅGÂ~LqÃ.%â‚%‚ ”ñêzX–E>o²iË;¾ø¨MÖúâcº…Ç•Sü7¡àaÊÆ÷ºŒŒ02e™8â‚%\f$]A„ª”Ç X–U(8èTÑêîWcccX–@,ó‹Ý[q=ŸÏFÑuãÇŽsèÐaXµzeÕ¾>t„cÇŽÑÜÜ̲åK«¶éØßÁ©S§™5{ .à왳ìÙó1‘HØU™¥pnÞü<9‡ÃLÔ$¸-“É`Y6ªª …ü6¶m“ÉdPU…ššš’ýÇÆ2Äqe[¾åõ~¸Ö-ˇpù"‚ ¸x‘œsˆxò‡OsäÈQþà ŸçλÖî`ù¯ÿãßÐ?0ÀãO|ƒ•«Vpèàa~ýëß°dÉâqÈþ}ûyã·¸þúÕã ={>fÛÖí¬½íV.\@ww¿ùõk¤êë¦(@¦É„ÈÞ½ûؾí}t]ç+_ý2ñ¸+*v|°“]»vÅøÃ¯­ÄâŽÌjåÅò˜¦‰eÛXÞ%¥ IDAT¶U‘:X²` —qÁAaB‚èçú”|Ëæ-þ>Ÿ|¼×AV®^ÁýñãÜ{ÿ=¥¿çÇåtOÅŪ¼ÍêÕ«hllÀÌçÙôîz{{Ù³û#4Mcýú;ˆÅb“¾·c»–,˲°-«ðÙˆð./bAa\‚‚ã\ÅGM"A__?{÷îcÙ²¥¼ûÎ& ]GÓu2™ŒßîÈá£lzw3óÚæÑÚ:€ò«_¾ÄÈð+V­ÀÐ+‡,ô ¿~åUÒciÖÜp=N z<†††ùík¯sèàaÒé4--ÍÜ÷À½Ì™3‡L&Ã~ð$ü§„®ëüãŸcxx˜¯ýáW©©IðòK¿¦ót'Ÿ¹s=ó´óÓç~Fww7é±4¡PˆM¬¿s=ÍÍMàÀ»ïlâÈá#,Yº„ÁA::pÛºµ,_±ŒÃñþû;èëë#ÓÖÞÆ­·ÜŒnTž«ªªÜu÷gxág¿äøñ]<õäFzzÎH$Ø»w¼¿sÂþäóyþáû?`Ç;ih¨gõꕜ>ÝÉþá)Μ9K$Á4-º:»8yò#Ã#SÖr|K†[N¤XMÀ3|H ˆp¹"‚ ¤œO–¤x<ÁõkVs ã/þâWÜ~ǺI÷{ûû˜–E}*Å_ü›?çÏÿâ_ÒÔ<£¤Íö­Ûq‡Y³fòçñ/ù‹ó¿P›¬çˆ.{?ÙKïÙ^âñ8ŸÿÂç¸mÝZ/^„eYlÛº€öö6Ž;αcǨ­©åØÑãtžî$ŸË“ªOù™¥þô›ÂmëÖ2sf K–,F×tÆFÇèé9S"TUåO¿ùÇ<þÄ7Xµz%;>ØÀòåKYµz%ë×ߎ¢*>|„¡¡áqÏáú5«ih¨GQUåŽõ·ûñ SAßHâ}š’K¸Üˆ – ‚ SÆ9ÇìP·ß¾Ž÷ßû€C‡ÓÖ6Y³gMºÏÀà sZçøU¼[[çÒÙÙUl3à¶™Û:׌+ sçΡ¿¿2ÆÄãìY×Â2::Êù›ÿVr6gÏœ ­½íÛ¶süØ F†Ghlh ­½ƒR—rÓÏkkÜ€îýàIº»{PP… ?ûרèhÉ{·µµºYª ýB,ÌβcLJn£Â¶þ¾~jjUÏAUUV®ZÁ»ooF7t–,Y\SK½U^aýB’} Âù"DA„KÆìëf1o^+Gãö;n›Ò>‰BÚØžîbÑÃîÀòTÛ”“LÖP[SÆ'¾Q¨*îZ¼TÁóÚZAQ8yò$´Îkenë\ÞÛþ>ô mmóØûÉ>zºÏÐÒÒÂc_ÿ¶ÿõoÿ+ù¼YyN55nyÇë¿sãM7°páWà`Ûµµã[rÂá0†î¦á• qájEˆ ‚ —”Ǿþ(}ýýþÀ}2V¬\Á›o¼MWgÏl|]×8vôxE›mÛÞãè‘cüäÙŸ’7Mººº'<î’%‹‰Åc ³yó-ZÈØXšŽŽ´¶Îå¶Æ"‘³fÍäÔ©Óô÷õó™;ï`nëzzz\KK«kñÆÿ£££tt cÿL³R|Tcé²¥lzw3ûöu‰DH$âôöö±oß~¾±á±ŠöŠ¢G|‹PÙVÄ”qy˜Š«šÃÉ‘AA.)©úóç·3x®dîÜ9|þ>‡¢¨|øá.:;»XsÃõ%m/YÄ=÷ÞƒÃûïï`hpˆU«VLxÜx"ÎãO|ƒÙ³fñáŽ]üäÙçùÕ‹/ÑÓÝCCC½ß®­­Í_ž3w.ñxœÆÆZf¶FWдÏogtd”W^z•ÚÚ‰‰ãP¼Ào\Ã-·ÜD.—åÝw7óê«¿eÇ;©­­­ÀjªF4EÓdØv))/¼YmºœÇ¹–QŽtZfö¢(rA„O+  *ŠR˜£ºë¥°­dm‚ã ‚ \Ýxƒ$Ër+gg2i††‡yíõWéïsc)xð~ ÃÀžBúÛó!—Ë16–¦®.9n›L&K.—¥¶¶¶¤ïãOîöL&ÃÐà±xŒH$2¥A£—9Ê.ÌÂrzt US1 cjDZ]w+ÇvÓàcÛ±X UUJÚ꺎¡Ø Êúã¹u9ñª¦qãMk0M“×^ýmáó´i¨o¤¹¹™ææI&k‰Åâ„B!4Mócj®e.·ºßßæZ¿æpò¤›`A\°AA¸" …B„B¡ ÛD"a"‘ð„mª‡iœÑ8áé©„u+@4=ï'ÛŠ¢PSSSñt\U]A£*ª«"œ?ã~6ã|¸ãµ?_,Ç™\`}­‹ ‚ ‚ |J˜ÐŠm8LUˆxB¡Úq'“ ’kQŒˆAáæâX=.¥­AÓ4ôŽ¢¨çàÊ%Ö ]3Ç©&,Šûgq'p™«ï_¢H( o¢(8Ž T³ ’‰ÄÈÕ.D$‚ ‚ W?ýÉÏà½íïûÕ˧Æä6¥l~±yçíwéííó_kšF$!O9C˜PÊD‚ÍÛfÛvE"Û¶K'Ç[–åN¶…m[ØVqišX¶»Í²ÌÊÉ´³,,Ë;¾»¾â= ý²mË_¯ßãÛÕj) ˆ ‚ W<û÷uÐ×ÛG¢P!|jöñ­ÓEQ__Ïö­ïñÐÃà>ô N­#bõ€É­ås`(Øw¹,~Ã_WõÝYÑ‚áþ§x]°|Œ¿\´~­%¾å£ŠÛØÕd"‚ ÂÏï^ÿ=wÞýÿõêëWùËŽãÍf‰D"ŒŽŒ’ËçH&‹©{†Éfs446ËåPTMU±,ÓÌcCCÃärYêëݺ étšþþêÑõÒa“eYtw÷…¨KÕ•lôS‡Ãa4UcÕª•lÙ¼¡¡aêëSWíÓëébb7«JP!( ©KE†íºhĉ›Rº˜îØ[†BwÇ»]­¼U 8Eâ~ÿJ—Ý}=­( Šª 8åÄmWMW(%ï]{r5 ‚ ‚ \‘¸)…þ~Nœ8ÉòåËüAÛo_û¶eñ…‡¿@oïYþãÿó]îºç.¶o{Ë4immåO|ÝoÿËŸ¿È{Ûߣ©¹]ÓÐteË—±ví-tìïàw¿û=œ:yš…‹ðàƒðÊ+¯²ãƒ4Îh¤¯·?÷+ Åß~ëvïÚC}C=c£cÌš=“û¸¡¡a~òÜOÑ5X,ÆÙ³gù“úG$nªÝÖÖ¹ìÝ»Ûo¿í\¯ŸfëÇdiq=±J!¦ÃÁˆ ¯VMµy‰8©&Dðê¾Þs‚ÏA) JQ\Ö+ŠêÏmÛ*¾¶Õb;_Œ¸í¼ó«&,‚Ö¿W"‚ ´£*ªÿÔØ{ZìÑÓs††† Ã(¶WÕB¡> ]×ÜÁ—ãðüŸßÁÌçùîÿû×?vœùóÛÙûÉ>vïÚÍ¿þ‹?§®>EǾý<ù£,_¾¬øÝg¸å–›ùÊW¶m{#‡ð¯ÿ×E$¡««›üý˜;wF(Äöíïó/þÅ?#u‹æry4MgÏ®=,Z¸ˆû¸Çq0-³äTU LZáoÄ.ü(¨š×Ns…„ª€ë=6®¹Ò÷ŠAá’¢(n°·¢žßà§ef ¯¿ö»ó~ÿÆÆFvïÚí¿Îesœ:yšÕ«W:Y¶ÏŒFÂá0}ñ ¥)O óT*Åg¼Ÿ>{Û¶¾Çï~÷íóÛQU•eË—²tÙFGÇxêɧ9tð0 - ¯¯††úñ;ë»a)•n>Ÿ&K©\/µnÕ4»É²­RâTIkY1S žøÈ™®kVH Ä{b1Ü8µ B _|+U‡Z°Ü'MÓ\ã¬#¶¦ª¨Ž†£T¯¢(UDQ•ëx%ˆ ‚ ‚ \Šƒ¬ ;ά™3%=–&‹Nm§À{ÞºöÞ~ë~öü ,Y²˜­[·F&ÜýŽ;Öñ£>MKK K—-Ŷm:Ä 0Í<ÇŸ ½½ ¦±±€={>¢iF#©TŠá¡!r¹<õÁÑ{¶—öùíS?ùO‘¯ÞEp^DîØØVuÑaYVa»…m;~}Çßæøõ9ÛÆ4-º³tõgéÈÑ3£g(ÏpÚ"“wÈ™6YÓö?0t…®Ö¢!•†¦Zædˆ¦:ƒ¦ÚÑV´z¨ªoq…†ªª¨…¹æ-[šª•ˆGUQÔ‚)XDT5àâHãëq¥Š ‚ ‚ \T.–ððˆÆ¢ÜxÓ lßþwÞå¦âmllÀ², Ã`~Ù ¾¡¡Ët·' ¾ý?ÿo¾ñ~¸›µ·ÝÊ«¯‹ÇˆÅc\7{vÉþ­óZÙðÄ×yëÍwؼi3áp˜ëæÌaé’%8¶Í¾½ûyó·Ð4¶ö6î½ïn²™,¯¼ü*ÃCÃ457q×ÝŸ¡±±Çq…JWW7ñ¡IÎøÒ»\M÷4ÈäVbZÜ`y0ÎÞ…£P°ê²íŠ’œir¨3ÍþS#tœãÈ™,¦5õëî9Óµˆ¸å1-Nö•º *@SÒ`Asˆ-a´D©‰n hMC+ÌÕÂré\CÓT,»¸¬¨*ަ¡ćƒƒª¨ØŠí[Sµøÿòwÿd²–L&áC‡¹ëî;™1cƄٖ.6MÍMÌkmgëÅWÁ§Ýã·)̧a zN.WNõøתacš&¶maZ®ðð—-‹|.ÏGdžÙÒ1ÀãŠMS‰EÄB¡NÈÐ …t"á!CÇ0Ü”ÏÁkeY6¹¼I.g’ËåÉfóîëºt&G.W´Š88œ09=0Ìo÷ ÓÚhpC[”5óÄ£:š¦ûD7 w®iØAb«¶­£jîµÐ4­ùK-¤ëÕP|kˆ¨/¾êAèÓ%BD€‚ ‚0!ÁÔ¢ÁjàÙlÃ0üT¶—¯à9£ÀŸüéœ:uÓ4ùò#_"•ª»ì¡K–,®H![ÉÅ"^œËèèXñè*%ÁÎ…B®{Îeˆ–º­@À²ä”Z<œR‹G¹è°,Ó­jo™®ðÈ›¼óÉY^~¿—þQ³j?ÂaƒúT ©’µñsÑš¦ÕBD#¡qÛŒŽfèí¦¯o˜áÑtñGÏæ9z6Ï‹ ss{„ûVÖ’Œ躎nY躎©iîëB º®Ûh¶ŽmÛè¶^ˆÑÐ4÷:zŸ­mƒª:.YW‚"‚ ¸+-«ªJ2™¤§§€A‰D‰où•J"‘`Ñ¢…nQWSç¡Kâ ×R448Xr o€ªøéa]kÉD¾/&å×>èÂæm¯p¹*‹ñðܫ̼+6LÓIJLLÓväÛÎÐ9«xÿšD”úT õµÄbá ûYÌ„å`;àØ…š"Žãf¹ò+›+~÷úÿâññx„¹×Í —7é릷o˜ÁQlǵƘ¶Ã–ƒi>8šaý¢w.K ›èºŽaq¥£ë®ðÐ4M× }ÔQUïšE›WÀPQlÛ¾¢DˆAA&Ä—¸±uuÅŒN=]=\wÝì+l0_ƵVBcŠç£(Š_¼q` (@´Bì€7¹ÉT?¾çRBÇs»*¥AæEñQ&·Lßêaš&–m‘Ïç1M“§†øÙ–ugJÞ#Y§©1I}ª†P¨rø["4ìb1ÃRk ~QÂà:Ŋ鎋aáN(d ª !C§¥9EKs ۶饷oˆž3ƒ~Šßß}2ʶCiî]ãÖq̉a„ˆia6ºîZ?lÛÆÖm ]/œƒ'R8+ÀñkÐãB\¦S„ˆAa\rMÓhHHWW¹\ŽP(ä?a½b Æã^Q‚äÒt¨ef ±xŒt:MçéNlÛ!)¸÷èz¡ÖÄ¥%â£pÚÕ-ÕãÏéÞ1^ØÚÍÎ##%ïY“ˆ2on3uÉêqCÞû9ؾÀ(~çú¹”îkÛ™¬E:“Åq dè„Ѱ®»)vêkh¨¯aîœ&ŽŸèñ…ÈHÖæ—;GØt`ŒVÄYÙÇ4MtÝ d[Ø–mØèŽî&[øÿÙ{ó09®ònû®½—Ù7iFûbY¶åEÞŒíÄ1&@ ŽXL >ŒI_ø’¼yÃ{ÁKr…Ä8`H€›‚ †€Á ÛØØÆ–dÉÚGÒH3#Í>Ó=Ý]Ëùþ8UÕÕ==ÒŒf´Ÿ[iººº–SU=Îó;Ï #^,ƒ8?DÞèr…¬êûªDˆ …B¡P(¦P® S.«kè™L–ææ†‡‡BðìÓÏòŠ_ÏVŸÖ"äL§¦VÑÐ4Q‘ϲ`á-–¾6½°).W–eá8–ecY¦aV„d4¦ÉêVá²Ìïð+B¬<ÏÇõ\†Æ&ùÎÓ‡Ù¸m˜ q2‡åK:hmi˜zjQ rñD*Ò²()–J‹.£B`˜:)GæŽD¢dÍêE,îj£ûÀaÇ@ƒ‰€/ÿbœ%;&yõeu¬ìH˼Ó¯"òÙšòš‚H|ˆXˆD—Qßâ §B„(¢P( …¢&B$s@4tCDzLÖ]x O<õ³0„d„Ý»v³jõªX„ÔêÒ|ZpÚy?Ž“Þ'åÉfhoo#[WvîÜÅàà¾ïc™©T Çqp[zA™ødIÞR75ï£Vgó*ዎ°š•'ì|Ï£äºléáö/–«Z¥›¥KÚéhk¬øý%EÇ©E Ïów'Ïc[&MYR)›LÆáÂó—0>1I÷þÃŒŒL€†<>ûد\[à×.nÀ¶ƒòý „-†Ž¡Q®Â££ƒNÅ­8Ù"D …B¡P(ÓuWÖuÓ01-‹¦¦&–/[Éž½»xyÛvŽ pù•—cYÖi—" =ÕèaÅí¯Q*¢·9\¯Hžý8Ž#Êâ¤áX,xñ…M ˆ×¾ ]—!ÎN§I¥ÒØ–]Å ïω0ªûzĹž‡Šו!Wn©Ä^àëOôÅ^Û2Y²¸… šÑOKáQMy\Å’KßáR)‹æ¦:Û¢¾.ÍÅ.cd4G÷þ~ÆÇ'üôå‡G=ÞxuuGæÆØQþJ ÅG ¢Ì~0Ãó!Œøì:úaRÝ7äDŠ%@ …B¡P]—Ä0 lËÂI9¬Z¹Š‘Ña††äÇÿóË–/¥­½ÆÆl{úÒ¤§ZÜ¢í¸÷—oó`¤‰Zgn4 FGGa_÷><¯\vÖs=²Ùzêêꨯ¯'›É’rl[ ‚UK|@(Úâ0¬@æx„ï^EÈ•¬tåy®[¢Xryè§=ül[9±¾£­‘Õ+»0 ½Æ¹N7áQ 9ÆÉB‰ÉÞ!2i‡ææºØ3rÙÅ+éíf÷ž^‚­½C?á­¯¨§µÁ …›¬f V”< …ïSMþd•¬du,‚¥P( …â”Q6L¤Ä0 ,ËÆqÒ™ ]x1ÝÝ{8x¨'™Ù³{/{vï›¶©œ¢BpDH#^ÍÖÑÐÐ@cc£ uu¤R)lÛÆ ÈÉo$>*ò=†‚~Ô@ÐucïGÉu/ðéïïcG¯ì¥¡i+–.`QWëÔãË¥“z]sGŽ7—/0‘›¤¾.Css–iй ™lÆaÛö”J}cŸylŒ ×dXµPĉ脞+ò€hÉû!Íþ¤¸ýB¦/Ñ{¢Äˆ …B¡P(ŽJâcC&1Û~ZÎJ/]ºŒt:KOÏ>ò“ùŠý<ÏüS3ès!ž`&õÙ,uõY›hjj¢¡¡L:M*•²¬°ƒ½~Bòvb÷ªâUTÞ6Y骜ó!=®+C¯öžàSßÛÏÀ¸ì*n™k×,©¨nuæ jäøÇÆóŒçikm ©1KC}†õ—¬dÛöÆÆóäJ‚ÿؘãÎK}®\™%ˆJ Sy„•üd‚&ÿ&“ž“-B”Q( …B1-qHå0,ÇqÂò¨„ß9)‡ñ±1ÆÆFÉåóxž‹ïûqˆMÙ6<ÓÃÓ(\€n˜Ø–I6c“J§Èf²ÔÕÕÑÐÐH}}õu d2G (ÿc¾©½š¦âU2ù¼R|¸¡øpq]—çwó¹õPpe²y6›âÂó—rìªóM¿­è^ÁáQ E—íØ¶ÅÅ-g÷Þ^úú‡ñ|ç…ýãw\*ï%‰{]Mü¬Ëé ±•BŽ;$q(¢P( …â˜DÉè†i 8ì  Ó²H§Rd³Y …Åb‘R©$ʰC¹ìií†tŠÙQÝ¥^› F¥vTŠL*M&›%›Í’ÍdÉf3±÷#*ÏšÌ8$ÅÇQ»›Gev}?ô|¸<öâz¼— 4ÈÛÛ9oU†^NºÏrBÆ~ê‘!£c9ŠÅ‹:[1MƒóVuQ_—fמ^„übw‰áÜ(o¾&Qv¸êoL6š”ÇŒ»·‡â• Ê )—V;‘^%@ …B¡P•hvT×uÀ4L°¥A¢é¦i`Û6©TŠL&K±X T*á–\<ß'ð}TÌ€+æ‡ÈˆŒ*•EIJdÉÝ”“"•NË÷”ÃG?ú1:::x×»ÞEkkë‰ ½JTTªŸQÒt ‚Ц‚®ëáy.®[bÓÞaúÙ!Ù²BÓX¾´ƒÅ]mÓœçÄsêª8KïÎd¡D÷þÃ,êl!vX¸ ™L"/d{ŸÏ¿8ÎëCíV+_€W„‹^ñsÓ€tC«¡É]NŒQD¡P( Å1IЍ\§¦ÉeC7d‡fÛ¡”.áz.¥R‰À÷ñ|OÎz²ÔjÒ¸QÌ MÓ¤‰¯¦é¡Ñ1 Ë4±l;ÎÙ±Ãå~ô£<üðüñoäÊ+¯äý÷¾ŸeK—ÍÛ˜*Ÿ­–³bj¹Ý@&žGáWž+û|”\—žü°'.³{Þª.´7Õ8ÏÜJ ŸÌTÚÈþ!ûaAGÍMu4Ôg¸dÝ ^ܼ×õy¦Û£­>Ç5«åöÑa#G‡†—ÞöºÉ IDAT¤ŽþÉð½ðY‚@+烜(¯˜¶oߨXØ9ˆ¦6OB¡8ÉhÈ€HM ßÑå:MG ¿«XNP* Å9B2”&šÁö=?Ñ¡Ú +ÉPšH|D3àsꙡ¨‰œÔÖË¡X†iHQhš&¦)— Èî4Mã±ÇãÿñilläOþäO¸âŠ+æ<–¤©zU»òe¨Uœë!“Í]×e47ÉǾ±‡¾‘Kµ±|é‚瘻øˆ„Q\¶6±¿G×oËa­#¹.>FyUç«êù“Ͻ:q!½ Az@ÂÂÒûá†M=~¶e ŽmqáùKæ.>Ž÷'wFüTå {û†°m“úº4Ë–t09Yd`pŒ#ょŸŸdÃÕe C×Ñ<é1“ßéšœHHæ$ÿfçÓ ¢<  < …BqüL',”×ãä1“8ýÙžçñõ¯x€õë×sï½÷²|ùòšÛNçýHæ}ÄÉæQÈUIV»*¹.[÷ óßÙ‡ Cç’‹VP—MU{þ<å±N㉼”×]SÅ5žJH´«Vt’JYøAÀæ-ÝŒOÈŽñ×­Ô¹m]VVEK¥I9Nèq0- Ƕ1M+.ÍlšF"Œ¯\¦Y¾—'fû{Jz@ôYí©P( …BQÅÑ’U“3¯êuâ^G ³ªõlf‚iš¼å-oáñÇ絯}-÷Üso~ó›yöÙg+¶‹§îÿÔ”°»(ç£BˆÒû!û~øä&‹<ðƒ¼°äÕù«Ï]|DηsìÝßçùºÎ…k—â8Oí ØÝ'ÃÜ"Áçz²ÏJ  ûõˆ "|rj¥*ëP( …â4ãD•íTœZn¹ån¹å¶lÙÂ?üÃ?pðàAÞ÷¾÷qçwÆÛ|é?¾Ä¦7ñéO JˆHã6)>ü°Lóž;ÌhÞdÅ«(Ÿ¡ÌI »š#':uýXÇ/•<º÷fÕŠ…ضÉÚ5‹yqó^ðã—K,k3cÁjÑ»¦ëè¾®šO i21=á“£:!}.ez•D¡P( …B1#Ö­[Çç?ÿy¾ð…/ðä“Orà 7ðùÏž|>Ï®]»ø·û7þðÿß÷ãôrÏ?ìûáÅ3ðÃã~øâ¶e²dQ{|®rÑ,8g¼ÕÈ{•Ëè94@C}†ŽöFúÇ`KOAzAJò%Ë»qXœŠÂ؈8w'›'”Q( …B¡PÌŠ®®.>ö±ñè£2>>ÎÕW_ÍÈÈÿþïÿÎ;ßùNiÌúÉð«°Vhäz¾Ç#ÏôSpeòó²%FuÒù,8gÅG%ƒCãäòE–-]€®K/ÅÏvú Q–|y¾ïyqId?âb2ü*€0¥VÑãÍíRD¡P( …Bq\ÔÕÕñž÷¼è,ÌòÐCñðq~A™ÿ‘èýÑ;8É϶É8,èhª:ú,Œ[%>Bä8Ø;@ʱèêl`tRãÙîbÜðQˆ¼ ²yh (w«Ox@„@Óæ§ „ …B¡P(ŠY#„`tt”7¾ñlÙ²eÊ÷_ûÚ×xÃÞ‡ùºžçñŸO÷㇉ç+–.¨È-8ÅÇ|fOårFFs€Ì«±,™öý‹nAn²D©T’×{²ø~ ="‡ÐE^êÊ]sÅøÀ>ü¿êê'QycŠs ©Çµ°K¨&^é iaI^­rYéw…B¡Pœã °zõjî¸ãn¾ùf®ºê*Ö­[Ǫիhhh`Û¶mìß¿Ÿ›_usìùð<—‡ÆøæS²a^ScË—vÇaÜž%âc~‘IãùÉ"í­ 2áÜОÀó5–µ…ev C7dóAÃ@¯ªXWQ]ÊŠj3é-SÍØ˜ìp¯ª`) …B¡P(fE$/^Ì¢E‹¦éû!gÖ]×ÃuKq’³çy<üÔ"­±bق꣟܋9+‹.GÆèho¤sA3‡z‡˜œ,ò|¬_Z¢Ý4q]Ó41=Ãõ0tßÐ #0Ðu !ôŠç;•îÔ®B¡P( …bÎ$H´ì‡¹Q^ï{lêc{o€MU=?ÎÐ«ÙØïs´õ{û†ð<MÓb¡çú°q·¬Bæ¹^"Ë«™ …`E${ƒïCPD¡P( …BqÜ$Ó@ñ{R|øa^Ï÷ùïçd蕦i,[Òqœ'ó°Ëœ–)³ÔTÇ„Àózû‡hm©§¡!ÀK½0šsñüPˆÄ"ÄÇ÷¥“b2¹UÆŠÆ9ÛÐ9%@ …B¡P(ÇE²Ù ÒP „,Á²H(>ú‡ ìî/ÐԘű­ÄqÎT—ÆéÍáQJ%Ùè±£]VBã¥C•âÃó£É "D¼,¡æ˜›jSD¡P( …B1c¦Îv—óDr9œ5¢²®¾ÏS/Ç{µµ4çŽìçò9 NÐÚ\î2¿µ²øˆ½^ܯÅü) "3ùŒç(¢P( …B¡8.’a8€ìž-‚°÷G9ŒÇd‰×§wŽÆÛ¶´Ô×8â±N8ƒ>‘ĶÍ8 k`B£o4,V'ó=é!XåÏî‹÷ok>Žð«¨QŽÏž>-³×k"Œ„^¶„çi×-¿ò|ŸÀbñ!ª’ÐEP; ëxPD¡P( …B1#ª NÙœ"u a¸NE–°ó`>Þ¯µ5~uâ½^©€[œ$ð½Šõnq²Æz[*à• _±}ir‚ñÁ^ò£ønñ¨ç" 0>L~ä0^qrÊ÷ïâ• Ç<Î|1vFO¥l²aéãñ¢ÆpÎ/÷m Kða'ô(|. Ã’×U)<ŽG„¨F„ …B¡P(ŠYSö|Èö¨ú•œ)÷+¯Ær%È.ØÙlŠ”cÇǘÙÉæ6Ö'þƇúé\y1WÜ~7ÇùéW>–\pënz#cƒ}<ñ­àò;ÞNë¢ÕäF³å§ßdìHOÅPRõÍ\ùšwaØéx]n¸Ÿ]Ï>Êh_wX=J/¥êšé:ÿ®¹ €Oý'C^Æt2\þºÌí‰`l,ï†NkK=¹œ¬F¶o0 ­Á ŸU'Ÿû¾z@‚ø9W†ÝiÇYKy@ …B¡P(3æh3à2$K–o ÂÜŽ Ø~h"î|~ÜÕ¯æ@Kç †úöÆë†úºË˽ååáÞpM£©c)/þèËŒ ô`XÍ+h]´šT]“cC‰µ0th7Ï=òFz÷ ÔµvѲø|êš;)æFîÝ}®ñX! ½ ÉgÐ3ª•ó@¼~²ˆ€ïÇ!u•M + Dç˜ Ê¢P( …B¡˜1š¦M-¹K"O zO4´{9~Õ|rïZ;W°ï¥_PÌO$ÛØ‹'Û@nä¥BËÉÄ"¥¡¥Óv(äÇÈÈæ‰Þø[t¬X{~ÆbZàºE¶?ñŸ`§ëXwËÝdšÄÛ–ò㌠ôœ”ëŽá‘ Z[ê¥*eS(”艈{¬‚ ²ñ Ìç){A¤ÐÐcÁ¡Mí‚xT”D¡P( …B1k´hö[£œ”ŒHÌ–—ó@ºÃ惺®‘É8ÀÉ ¿‚²`(ôp ÷îÅ´–\ C¢†CÈp¯|oZ¸ ÛÉb§²ô¼ü Göm£Tž„úÖ.tCÎçÞO1?ÀÒK^I¶yaÅìt­‹×ÎýbŽÁèX.þT_'ÃÆò%±É²·#å XòY–½Yó•¢< …B¡P(Îz\×å@ÁwÜÁêÕ«OõÎ8¦52“¹a¹Ö ž5—y ‡Çd’w*eÏz¶|>HÕ5’©o&?>Ìp_7KÏ'76Dû’5´t­~Ìpo7Ù¦J“²ZTóÂåhºN×ùWÐýâã ÚÃС= i´-^â ®¥¹s%Ãýñùê[ÅË»ŸùoH!%`ù•·£i§Æàû¥’‡m›dÒN¼~0'hk *ÄGÔÄÌ`jæÌþy*¢P( …â¬Æó<~ã7~ƒŸüä'|æ3ŸáÙgŸ%NcOÅtL邨ô€„á: O”(yræÌPow~ÕÒ¹‚接hºÎP_7ÙæŽxûH€œwÕí´-^C÷¦Ÿ1th¾ç2p`;Gö¿Ìú;ÞAÓ‚å•¢"a¤÷ízN扄«–]~šqê‚& %lÛ$¶ãuCy'ž—›†aXág¢çKù¹Kñ15äX¨,…B¡P(g5õW‹€mÛ¶±iÓ¦S8¢3˜*;3ž Sùå,!úÂêWP-@NnóÁ( kbä}{¶Äë ˦¡­‹±C9°C޳¾'SÎUÑ‚ä²_7¿í/¹øUÐt€þÝò·”m*‹—ñÁƒñ½ºü5̲Ko9êØN¦S¨PÏ#* ‘¼^‘ó!û·„^¢*g‚èÃÑÑg‚ …B¡P(ÎZyäþîïþ®b]&“aýúõ§hDg6Ú”P,)8(çÄÉÊ2‡ w¸Üç"=[Ȉ ÁR( …BqV²oß>~ÿ÷ŠAô÷ÿ÷ض=Í^Š™ i„U‘Dn$!:Q)^!ƒcn¼Ÿe§jÈÔ5µã¤³'s!h^°Ý0)8^üyÜÉ=)@ßcç/È®ç~D¦¡;SÏÄPœ+Ò²è< ÓæüW¼ŽM?ún!ǯ¾ÿ92M °Óõäù!§וÍ CG×¥çc´ •Kï Š ‘|.s2ü*‚³«„¥< …B¡P(Î:J¥6l`hh¨bý›ßüfÞýîwŸ¢Qù”ÎäZ-Î)‡èÈ÷ ˜tËÝÄS˜û•^Šå„à€JbZ]ç]†ådÈfèànJ“–ÃÊËoeáªKËûu®äÊß|Í«ÐtƒÜPÃwPÌRײ—ÿ:ºa°ë;6¢¢oIô< žW¼"({µˆžubå€TŠ˜jåQ( …BqÖñ¡}ˆgžy¦bÝÚµkyàNшÎBBc³ìItF'*Ý ·l¤žjrÅío«øÙÏv:Ë«ßõñDg÷r²µaÙ¬{å]!pK“ò㦕ªƒ!i™Æv.¾ím!˜œAªGÓõøÞ!8ÿ†»!M'/Æ÷ËÄ4t\ü@«èñ‘,¥\—ea9ÒˆRÒ… …B¡P(Î*¾ùÍoòÏÿüÏë2™ _ÿúש«›ÇXüsˆšqýS g­\%)4Xƒ  PJ¼ÆôÇ«yüÓ ÓN‘µRBeZ4T]Sìšm¯ŒÙ0SÃ*H$…€’’¢DU¿Jz½´)}@f;%@ …B¡Pœ5ìÚµ‹w¾óSÖßÿý¬[·îŒèlG‹ß…òS… nÙàÕgã™U}Î2û›äµŸGX)9.( ¡wÔš6;¥r@ …B¡Pœ îºë.ÆÆÆ*Ö¿ãïà÷~ï÷NѨΠª’X›$ÊS6te‚ž2*Q9”ªäEádSû}”s?Hîœð„¨> …B¡P(ÎAî¹ç^|ñÅŠu—\r ÷Ýwß)ѹAåÌ·–X/ÿ§Â4 7µLíØU“”÷#æd´ ‘¥vkW¸Jl4/cRD¡P( ÅÏ—¾ô%þõ_ÿµb]}}=_ÿú×UÇó“LµÍj[es3úJÓ ëœº’¼g7lj¯“^ ˬ¢Zr€º–…èú\Læ™åi¸… £€F¦¹sç;:ÉJdÉ„tË8y.'%@ …B¡Pœ±är9îºë.r¹\Åú÷¾÷½lذáJ‘ı¦¼QÞ€mj P*÷ "à©ÿ|¯$;¨¯ºì&­¹ü„Oˆ€'þ°î¦7°øü«9|€g¿ÿynØðAÒuÍóv¾#Ý›q 92M ©o_¯Ø÷û_øºiqùëÿlÞÎWM2ÿ&zš¦VÙLðX!r³è;8%@ …B¡Pœ±¼ûÝïfÛ¶m뮼òJ>ñ‰Oœ¢{Tª"6L5MæyTz@‚h«x]ÊÒ A),Ã4p`'C½Ýñ÷Ý[ž:¡¤–“¡9lT¨ók.Üú¹‘Ã,<ïª bg¨o_†nœØÐ´ZSñsŒž[ø¡r繨ŽJ€( …B¡8#ùìg?ËC=T±®©©‰¯}íkض}ŠFunRs¶<,ãš²kªÊIiÛ@pý€ÛŸÀrR¸ÅÃýûÉ mj‹·éß[œÄÉ6®kbàà.¼R¶Åç‘®kŠ·<´›ÀóÈ4¶bÚ)zv‚´-Yƒ“ž¾'L¦¡™•—¾Xvªâ»ñÁ^&†ú(òØ™zš®Àr2¾ÇØÀ!ò£¸ÅItË&U×Lã‚rÇõáÞ]øn €bn”‘ÞÝ –S×Ò…qMu‡ã‡÷QFÓ Òí¤”ï©[ ?tdZQÌ’ìÁ°ÓÔ/X-Ÿå °ZıD,<¢2¼Q…,MÓ@Ó**fÉq®Gbev•°”Q( …BqÆñ /pï½÷NYÿÅ/~‘+VÔØCqbHTE U4-|“ïí e1Xre¾Gu³¦AÆ1/п÷%λâVv¿ð8Åü=;žãü«o·ßúä# õí£eárŠù1rcC˜–Í•¿ñ»´->€~ô5&s£t,=Ÿ‘Ã=” 2TÏI×qÍo¾“ú–Ú¹£yîÑ/å,ßsÙòø7âñÉë,¹è¬¾ê7=¼Ÿ~ðïñ=‰ú4Öµ-â’[tƒ­?ùr¼ïðÁ Ü®|ýêyyJVqb˜]O=Ìäh¿lf¹iÑ,½üNÐt cìýÅ7@óÒ‹Þ¿9î®^¿`‹¯x]ŵY¦ô°x¾OöipüX?hr9|~KÔ›‘™)ª –B¡P(Š£r";8cccÜu×] …Šõüàyík_{ŠFuvSÓáÅÿCØ®-üGbÖ¼£¡<ß=9)ó:üišÙMôíDÅK×y—ѵêz¶?O­Dnš;—sÑ ¯%mÄsKlzüÛˆÀ¯ØîÈ,:ïRÖ^óØNšâä[~öð¬îÁŽg¾Ïáî—Ð4…«.áâ[ÞÌškî$U/óC,'ê+~õw¼“+_û^οþ·Ðt‰ƒÚþ š¦±âŠÛ±ÒYêÛ—²lý¯³|ý¯£›µ=vÝÏ~ÉÑ~L;ÃâKn¥cÕ•hšÎÈÁmôïüÅ”íÇzwÐqÞu4vJ6Þ¿›âÄ`Å6©”<×äd)^×”’ÏN×õø¥…ÏWÓ´HÄÞŽéòCf¡¥< …B¡P(*©Š¦8f¿†“ÌüÁ°{÷îŠu×_=ûØÇNшÎQB=ô|ÈÏ”V]cASٸ·$ìm7Å`íÛ³™æ¦F´t3©l]«/cïæ'™œaðàZ­ªØ>]×Èe·lˆÇ±åçß!?6ÄøP? m]ñvÍ –rÑ Òà'ÙýÂã õíÃ-NbÌ0\¯o÷&y¬Î•\ôÊ»ÝÞ¥k¢®e!h:ƒ=;p y|ßÃJe)åÇÉôеöúw?Oir‚ló:Ï¿:öVTSšg|૯¤cõU!Èô11x€‘žm,XóŠŠ}:Ö\GëŠ+)Œ2Ú»C^o~'ÛR¾g±)ÆëS>šfÆ÷Q"‰¦ëå0¬ˆ™ ˆB¡P(ŠJN/½QÁ?ýÓ?ñ­o}«b][[_ùÊW0MeÖœhdŽ€¨eχ…î€4\Ñh̘8¦FÑä³î‰MŽ0Ò߃®é´´¶ÐóÒS”\Ý0|ŸÛŸ›"@Z»j.rc¤¡­3±]gÅvY»œ[Róš¯TÀ+MÄÉéÕÚñ,ÛŸü. ÐM 'ÓˆW’^:Ï-ÖÜçh”r#ñr¦iabyƒ(åG§ì“mY €a9ñºjoPMHZÄžÐÐ5]æw@ų$ÊI$¬'™©³T…`) …Bq®3¥Ùqí.ȧ:ëé§ŸæCúPÅ:]×ùÒ—¾ÄâÅ‹OѨÎMDµ÷#¦‚¥åèt4JqX(”¹s¢½»7ÇËC÷Ò»m#{žÿ_Ð}{6Ç Üc‡j.§ê§ßn0±]¶aF×kÚ),[6´îÝSs›ƒÛž ¥k5׿éÃ\ùº÷b§²S¶Ó4™ƒQ- ª±³ådúüH_bYzSìL#º®a›Žc“J9dÒiR¶‰c[X–9E †mËg‘Ox@Z2òY麎aèhº|nš®'žeøt5-~—×3û 5U P( Å9H$&¤9u½œå®4,¦ÎvÎ- c6 ±aÃ\×­XÿñÜ~ûíÓ쥘oDz={Ù-[‹Σw]Óä¬y¾³¬ÍáÀ ‹‚\¾@}]ÏØfù÷Ó»g í]t†¹£££üêñÿÂsKôîÝÂâDIÞÉÜ(Ïÿð!Û±gÓÏÈ6¶Rß\®0Ü¿ŸM?ý©lû·>@k×J,'M Ž."®¾Œ[Ÿb¸w/›û íË.¢˜Gh°ä‚ëp²L ¦TÈ1vøGl£012å8 K˜îe¨g¦“ÁN7бjý”íìt=õmK8Àá]Ï¢i:ÂÍ£•†iij¤ë‚kh©O‘wÓ´4K±RŸu°Ó®áÐÞ*îښëH×9¸^@&›‰?>!=:YÛ'kæ}h±‰–e*H‚¥Mñ~ɯgÞ?”Q( …âœC$â·cQ«ŠfäðЈ Íhùd"„à÷~ï÷Ø¿Åú[n¹…¿þë¿>©c9ç™R½**$ÿ'9s®k†®cè:ç-L±qûƒCãR€åƒô`r\ëK.¼š…Ë/ˆÏ·èß³‰Cºéyù¹ ÒÚµ‚±Á^…ù–â’›[ÎÜ'èX¶–þ}[)æåRÙÖÝøúY]úš«n§T˜ o÷fwoåp÷Ö¸ Àâ ¯e¤¿›‰¡^^üá¨o[L]k'ƒ½Çi[¶Ž¡ž&F8øÒFœºæš`ù•wrà¹ï!ŠÃxý›Ñ ƒºl†ÆÎóèXuÅŒÆmh–©c:m-õ€Å¢óu^Eò¹ò¹é¡K×dXV¹:V9OäxÐöí ;Ñ´Ó«Ê…BqòÐÐC׍ކ.×i:Zø]Å2'¶QB¡Pœ„ P®ó_þª¶ÜNÌ1ùt6¸®Ë÷¿ÿ}þõ_ÿ•Gy¤â»ÎÎNžþy,X0ÍÞŠù&)<…!‚€ ð}Ïóð<×-Qr]Š…"¥R‘ÉÉI†Ç'ù˯@Èd®¸Tö§È::¦¡QÈ19> @cûâÊf|rcCä'FÉMz4-\Γÿ C}ûX¼f=ënzC‡öà–&i[´'Sïúãÿø“¹QλâV\r#C‡ö „ mñ*,''€”ë³mØézJÅN2QÈÕÔ/ÂÐ+= à gÌ£uC'ãt5YvÉç‹LNI§\_`©lÃQó1² -dZh/zß™–MDzµÇ¿Ê°p庚ߵv­Œ…`9iZ:W†½#ƒë]ïzþç>ës)æòsJö…('(Ë™òʦv†¡sÙ²,?Ý&;‘ޱd‘ô"x¡d¦hÀµw¼•+\—Rpô‚®×¿á½"À²SÇw±'ÃÂñôØ{qBR¬4LFÞƒ¤÷cEc C70tÓ4Ð ¹ ]«|ÑËáVª –B¡P(ç*"¹(¦ŠŠHL„âA$€“ÛG "ÜŠxUxÜdå,Õù$Óáyßÿþ÷yðÁùîw¿;¥›y-Z[[Ù°a¿û»¿ËÕW_=£ó(NÉÑç²Aª~˜´,gÏ Cæ ,iOÑ^opdÜg<7I±äâØV†5œt't”I1£iAh`FçJ†_DI¸µ< ûöí‹ó:¶oß>£K»þúë¹ûî»ùßùš››g}k'›ŠÊ2DKäèrF]7 LÃäŠY~¸)‡@084FWg Áq„aU£i¶uK§äg*DæÃºŸ–¡“² #ü»œï±e[MÓÈd¤*‡_­j)…‚ÃÀ0LÌ„øˆú·è±øÐç-ÿ”Q( …âÌ`º¼ŽèsèçAeˆ•""ªÜ£%*aEûTÙqù 20çS·ã›ßü&>ø ?þøŒfxW¯^Í[ßúVÞö¶·±råÊcn¯8uTˆT¹&ÎÑ5¡Sò?ô¨§„¡ÓÚà°¢ÝdÏ—Ññ<…B‰Tʦà êæ @"t R‘ñŠnP34ë¸8±b™:iËÄУ¿»“OCc=F~Õd]¬nó1MÓ41MÔBDO†c%°"Q©…ÿ݈r}Ž%@ …B¡8]IL.OÉ뿯îÍP±_´NÔø>Ú?Ὀ½&I›BK.jS×kà¹=ö_þò—ùÎw¾C.—#‚£Š––6lØÀÝwßÍu×]7ƒ›¡8¨ndY~éèº@ô8ôJ×}ŒÐb·®«gÏO†Bнÿ0k×,Æ®'°Ì£´³šŽ¥c[:ž/p½×NJx–¡kX¦mʲµÕ§SÆJtYóïŽ1 ƒ†ÙåÈÀ(aøÕšÖ"YG )@ÌðÉ—QÑ”PŠK-l4)ÿKp”˜Ì ˆB¡P(§#Õ¢!±\aÌ$‰0©éÂ<âõIåÒ»‚8<+Y†7¢¼íÖ­[ùö·¿Í7¾ñ :D©TÂ󼨭6ºlÛæÎ;ïäî»ïæÎ;ïĶíãº5ŠÓ‡)¡~QO­‚%6뺎a¬éʰfá8;ú\Ž Ž²h¼•úú47À2Ñäw–6ºX††eŒXŒx¾˜gdæ'3t S—I冮…ƒó_Jw¶46Õ£ëAнï0¦.XßYÄ4Ó•âôâp¹8t®ªÑ FùÙV‹ÙxC”Q( …ât¡ª’Õ´"±m²G…@©®d•ø.Fç‚$› &ò<âÄtMî044ÄOòSùÞ#lÞ²™‰‰ òù|EéÎ(Ÿ$2N¯»î:î¾ûn6lØ@KKËœo“âÔRK\&= š.*°´( Ë41<é¹ãÒzvö !€=ûú¸tÝ EWàXóÓ[fʸ)‹@æžß—‚Äóü$Äëº4ÄuMÃÔ sW´Ú§˲Ȇ½?õ Q(–¸¸}’ú´‹ø•ð€Lex+Eæñ ˆB¡P(§ŠDˆ•üX"mV:EeµªjÃ'ZU² DŸkJ3ÁHl²³qÜõ\+_,Ù¹s'Ï<ó Ï=÷CCCŒŒŒTÌžVÏ”®\¹’·¾õ­¼å-oaõêÕóv˧‘©ÃBÃ0t¡£a,ÓÀ B×4YÔšâÒ¥6/ì/16žg`pŒ¶ÖŠ^€mÕ®§H%] ‰n Ìr•¸ ¡#sšŽ ‚ѶQÏŽÊNé§ MÍ hš†ëúè mù¬ë,ai,˲,Wñ·lĽAÊÝÐË èG-‰7C”Q( …âdR•×1Ū¶‘ôfD«’‰À¢R¼Ä½>´rÙ]ôò±k‰žž6>±‘Í[6ó>òÐ…ÎØøû÷ïgûŽí 044„mÛØ¶eYØ–]a¸´¶´òšß| 6lPyçSð¤'dó¦ÍÜwß}|ðÏ>HçÂNü°Ü«i™˜žÉí—4°¥g/tïë§¥Y† Ý€”}ôƒ'²z•¦iÑŸKY‰(´ñ$•Íš#©”C*-îï9‚çù\º`’”mJáy>, Ó´dºVÂJ$ ë±æ~J€( …Bqò¨‘×­«M%r/âÊVZ‹!*U}ìøc(<âp« ë»wïæPï!ºººøö·¿Í‹›^DÁÆŸodÑâEŒŽŽ2>>Ži˜±ðp§,Bl‹t:Í 7ÜÀwÞÉ­·ÞŠc;s™UœÁDÆéøCî¿ÿ~ÚÛÛyÿûïaÅò¸n ÃÐ1- Ï÷0M“–z‹kW9lÜY`²X¢·ˆE­=mI…âØÔ’DM LN–èí ÁñXÛîaY™Øûa™ !bZåð«Ð«™ìïl®áW ˆB¡P('ždˆ•¨š1®‘÷ñ *§cŠneF2”«V.H‚b±È}÷ßǯ~õ«xmÛø¾ïûüìç?ã-o~Kl¤DÂÃq[¾_tÑE\uÕUÜtÓM46ÉnÓóÑ!Yqfü kšF±X䡇â _ø×]wŸþô¿°`ÁB|ßÇuÝØ¨5ü@º¦‡eYÜ|a–g»‹\ÁGXÐÞ„iL–²Î©ó‚œÉÔÕg±lÙ¸sïþþø9]Ñ5{/M3ô‚X–55üJ×ôª°J½Ü¸tPD¡P(ŠA2,*üÁ”mãÅ„‰?'LTŠ7Ž»:–­ÌgUˆÌ}÷¿bñ±bÅ ^uó«xùå—ùÙÏÀK/½„뺱‘…[µµµ±~ýz.¸àÚÚÛ*gAç®8CâÓŸþ4<ò6là{ßû™L† ð}¿œŒ®é˜†A`øòweZø¦OCÖᦵ)~¸y×÷Ùwà«V,Äó“¥€ô) Å:qR ¡÷cd4Çà l<Ø‘uYÙ*°,3žT0Ͳøˆ’Ðe/=ÎëŠC¯ôrò9TN6¨F„ …B¡Pœ ªó:ª¿cz kºL|B¬âÝd(±èÐ]ÉwíÚÅòåË1M³âÉjX£££l|b####¬_¿ž /¸0>ö3¿|Fž[×ùП}ˆt*Íõ¯¸žÞ¾^vìØç{¼°é.¿ìrÛaÙ²e\qÅ´¶¶¢ëú‹¦kqiÞŠjZ5òMg»wïæ“Ÿü$[¶láÝï~77nŒŸµø²j’¿•¨H ™cà±Ñk™7žßÀ³{Š åõR_—¦£½‘’'Ðus´Þ  DHˆi™4·6¡iP(”xyGüB\µ¨€eÙXaÞ–z>,S†aE‚ÃÐ++`éFÙó¡U$¢Ïq¬s>‚B¡P(ç"Õ¬¦K¯ñ]ÒklاQ6ä /HX÷—¿ü%_ýêWéëïã}ï{×\}Í”ó vïÞÍ?êŸÉår<úƒGù£?ü#n¼áF@ÎZƒ<·eZñ,ç7ÜÈÎ;Ñ4_x‘×Üù²Ù,º®W†‰…Ë9&‰ž!ÓUÜRœù<õÔS|ò“ŸÄu]î½÷^î»ï¾ø;Ùé[vÈD¹sd̺A ûè†^N~ö=RŽÅ[®kà³?¡äÃÎÝI9  ¥CÓÃR·óÏÙ¢]t]§¥µ]×ñ<Ÿ—^Þëz\Õ™§³I«§´L+$aˆ!Ÿ‡aqIí¸¡6½÷ãxQD¡P(Šãeš0«ZÞŽŠ$ó€²ÐHˆMh±!w>+[Y¦E_›7mæÚ«®E Èår<úƒGyâÉ'X½z5»ví"ŸÏ“N§Éçó|ù+_æª+¯"å¤X´h»wï`ûŽí¬»h ±q¸ÿÀ~r¹uuu@Uôªk*_¾@JtœmAÀw¾óî¿ÿ~V­ZÅG>òÖ®] ÔÞQß ]h!= B‰e ‚Ð b™°¤=ͯrùê/rB°uû.»x©”M>Ì1Ž–•ž¨Vu®¡iÍ­M¦‚—wôÏXÝ2É%]ŽÂ¶­rò¹-= FTÅΔÝÏ“ý|bïG¢ônäIžûx0>ðÿo€ºúÉ£×\V(Îj4dÊÈÅ%Zé iháwË#.U¡Pœ}%¯£¦è¨.³›\Nxª+[EçÐÿ„´··S,¹û­wsË«nÁ÷}t]Ç÷}>ùOŸd||œP(ø“ü oÿý·³cÇ(•J´µµ±bÅ FFFضmÛ·oç²Ë.#ð¾ø_d|l\ž[ÓhnjŽ ÍhÌÉ&†ºžøï žðà„â©–ÈRœXŽÇ ÷¹Ïqï½÷’ÍfùÛ¿ý[Þô¦7ÑÖÖVó|q`ôükE ì@zÉdø¡ÌijÍêˆÀcï€OàŒŒäèho”³ú¾À6ue«Ö ±©TÊ`÷Þ>Ž Œ2ïãU+ ¤›T*…ãD/ÇIIoH˜”n…¥xehœ,—l„]ìµ0!Š.è’ÙþÞÆÆd3DeA) …BQ‹¤(ˆE‚"Q‘*îÇ‘\ŸØwº Våïˆ`Šð‰¦iòk¿ök<ñä¼÷}ï屟<@:“æŠË¯ˆÇ·zõj.¹ø Ãà5w¾&>åÆ'6"„àö_¿úúzùÐÿû!î¹÷öíÛÇ›ßôæøºöïßï[íé¨pˆ„q,âûÿSä¤ßÿÄëhìÛ·›o¾™þþþx]?ÿëý/n»í64MãÇ?þ1ó7CGGÇ1Ï_Î(¿¢™t#¬„UNv60- ;,r`Y6¯º°Ž‹™ A~²È¶=áßäŠÁ1ÏÏÔ§?so]}–tFöû8xh0.¹›±<^µ"O:UY:;º×¶e%„‡‰aTöÿˆ*_E¢#jX:×äó%@ …B¡ˆ¨ÑK#)’Š)Â#œùÿåЩäñE ¿‹Œõ@H£*î\ž8~Å>áùÆÇÇùŸÿùÆÇÇÙøÄÆx›n¼!Þtt46/¾øbZš[B°cÇŽ !“Éðá?ÿ0ÍMÍñÐ Ãà oxë/[¯ËåsSîOœ÷Aùzƒ (ç«$Æøá5>A¨×‰~‰òruÏ—Zb¤»»›W½êUìÝ»——_~™mÛ¶ñGôG¼õ­oeýúõlܸ‘÷¼ç=¤Óé£þÙT¢É\¨–¬¤TNr6P€Äð¢êLo¸¢Ž®&™w422Á®=½ø W fÖu|."ät0‰±eë²ÔÕggï>)"M=à–•4dÊý{¤ø(÷ï1- +,½k2K× 4]»ŸWw·ŸOTˆB¡P(Péµ8Z^Ç4ÆÄô­ Jœ×’ôt$¿cçž~úi¶nÛJ__ï¿çý\vée¬Z¹ŠÎÎNz{{Ù³g‡ÉF‚¯»˜††ÆFÇ8ܘÑÑQeŽë¯¿žï>ò]ž|òI^ÿº×³tÉRþáÿÀæ-›)ЬZ½Š xüñÇc#çâ‹/&1Њ±W„ÛhåRÀÕÆnvsz[ugò'g©+ ÈD…4€½{÷rË-·Ä^®/~ñ‹>|˜?ýÓ?åÚk¯Ã(ʹNÕ½B ]GèÓ0ÀŒ~+ °Ëâ)“Nñ¦k|>ûÓ &Š‚¾þaÒi›Å]mx¾`¢à“9VNœ”¼ŸÄ>õ š¦ÑÐX†]Mä ¼zŠÐ7,ͳ°!Nèñ(‹˲±Ìrïn%ߣ¤ódèœóåý%@ …Bq.“ðhTƒ£T¶ŠÊŠV؉㉩ûU—æMz:ª×õîãÓŸù4»v튿3tƒgŸ}–Ë.½ Üxý|ý›_à‰'Ÿà®ß¾ ]ÓyÅ+^Á£>Šø<ùÔ“¼úŽW£i7ÝxS,@žxò ^ÿº×£¡aÛ6«V®Â¶m9Ì¿}þßdX—kÏ_Ëm·ÞVYÍ*zÓ*Ç/½AÕ»Oèe"ÃÃj$Á(æ@x?C%—+U’Ñç]»vqë­·ÒÓÓirr’óÏ?ÿ¸ÅG,:Ð*?‡çAháßN€aèaÄž2Ë4 l;¯moº&à‹sxìíîÇ-ù,_ÖA€F®vt¬™TÇ:^•p–È2 ƒúƆ¸÷Ðð8ÛwÄ÷¥'u}Ç$«ÚE˜ëáà86¶ccW…aÅÕ¯L+Ìù(7¬å÷ù›HPD¡P(çz!æTeiÄåfùÉ&€S„G¸ÏÑÄEõúhßÈXó<~ì£ ÑÞÞÎë^û:.^w1­­­$Æõ×_Ï7¾õ „l|b#o|Ã)L}ôQ4Mã©§žâÎWß @WWëׯgá‚…q)Þ(Vþ[‹Ç~òX|m–eqË-·°áw6`Ûöô·1MÎ^û?ð}<ßÇdWõh†;)@ä½Sdî„^dA д¨‰œÆõËpÓ4Ñu^xÛn»ÑÑÑ)GûøÇ?NSSüàç8ªdu4„´A×A``„?‡@LÓDDùCA&¥¬èüÖå?7‰' çй|ó×,Æ2 òÅÇÒHY3È(8 ªdY¶M}Cš®#8x„}ûÇߟ×Ràò%^Xñʈç€Èœ;,·[®~¥‡aqÑï&êë3µì®*ëP( Å̈lÝi„G¼®F`yT7"jX]Ý*ö†$C©¨!<± E¶lÙÂÖm[9tè~gË—/go÷Þ¸GÇ[Þô.¿ürô`ÇŽ¸®Ëùkϧ£½ƒ––Ö®]ËÖ­[dûöí\pÁ,[¶Œ¥K–²ÿÀ~övïåàÁƒ,Z´€?ýÀŸÊ1Uy3nºñ&¶mÛF{[;]t×ßp=-Í-r»ð¾Å]ד¹šÀ÷ýøåº%\×ctl„þþ>††‡qÝÒÔþ3Ù<-‘Ïɶl›imi!Î`˜¦a†¯e~Åðð0oûÛéììäÖ[o¥­­–––)¯B¡@*•šýHª¼ Ñøt]#*û‚]”½ fT°@ ˆðoíÒå‚ú´Æ×~1I®$™à…M{¸pí²™EWà{†²f)DN'H*“&S'ó=|?`Ç®ƒ „]ÎÑW,ÌsÙ"Û®¶ð|8aèU˜{cfœ‹S½2dùäP0Æ÷ó|#”Q( ÅÙËQB©*ÖW‹”ÈðFÔ&q¨U•à»&bí«Êк®Ë·þ6?~ìÇqŠåË—ÓÙÙ‰ã8‹E>uÿ§âDõ˲øËÿï/Y±b7\C\J÷‰'Ÿà‚ .àÆoäùçŸç†n ¥¥¥"?ù]çš5køÄߢl4ê•‚AÓ54! ’de+Ïó‚€’[bb|œgž}šƒR(LÖOSœ4R鋺³æ¼µ¤œ¶cãØ õ<ùä“X–Unó3«]M2ï$ÅÒ@øacÂn=eÒ#"ׯè€w¾Rã«Oçé %^ܼ—5«ÑÖÚ0»¼xpáûÑ¢OÂÏ÷Xv½¦idê³ØŽÌ÷˜,”Øúòþ¸Ï‡eÜ´l‚å­ŽJx=R¤BâØ‘Dz?ÊáWfèùЧ„êMù=ˆòxæåº÷í ;Ñ4õ ŹŠ€Ž¦…ïèr¦£…ßU,cœÂñ*ŠiIäuÄŸ«= Õƒ0¼ê(†sTå©|ši<G1º?öñ±uëVt]çÚk¯å’u—б ƒºº:ºvð«Å'ÿñ“Œév‡L&ÃÐК¦qÍ5×ð¾÷¾B¡Àÿ?L©T"“Îpÿ§î¯™ªntÚŠEÑ÷µ¼Õ÷Æó=ùr]víÚÅÓ¿|ŠÂd¸"VBô¨nè'žéÊíf2Y.¼àBÚÚ:Ȥ3¤Ó)R©vØ ;J@†ù}VSr … ¢ß‡ïËÊh¾ïÉ=ÏÃu]J®‹çy”JEJ%—b±H±X X,‘›,ððsy¶÷•Ëñ.YÜβ%±÷Ã65Kg¦:DŽ/zE%¤£eùe$Æ#/Mùz rìU^HnKc—‚•ç+o§á86©t -‰Ã#¼¼£Ïó¨·}^µrœŽzSæzØ ñ‘rÊŸCˆeYqÙc3NB7ÑdB- —«-DæòzzdŸ!åQ( Å™Ï1«" zŸ¨¾ý´ÞŽªp«éD{¶¿{_7[·nà]ô.®Åõ `dt×u D€®ë\véeüíÿý[FFFhnnŽ{/¼ïýïctt”ÁÁAR©×^{-ù\žo¼16$ãñ%,°dbprØqõª„ȨUí&¾6Ðhô)‹lܸ‘]{vÄex5]cñ’Å´µµÒÜÜ,'%@¦2SmÚ/g@n"ÇÈè(#Ã#ì‘Fj>Ÿãù_=ÇŠe+Y¾|ž_ EbSÑPò’ümE猼 e!€ËŽ rrôï\­ñ“­yžØíË<ˆž#ärÖœ·Ë4(y‚’çc›27äLüùÙŽƒ“N•ï‘<4H÷þÃñßâ‚l‰›WæhÈÈ WŽã`;)Ç =aVäñ°¢Ò»V˜d¢ëå¼ùŠÂ°d‚X²Ì¯—L …B¡PœyÌ!¯#)<¢¸ôéª]MI'!<¢%úÌÛwlçÑ<ʾ}û¸è‹¸ë·ï¢¡¡!nüðŸßùOûÉcØ€ü¤ Åê\ØÉ‡ÿüÃ455±páB.\HìíÞË£ÿ?{ïÇyßù¿§lÃ.:H€ …$%6‘–Dª[]¢¬^HIî–Øgßå÷Ë+Nìó“_â\bÇ9'w.‰­.«Y%“²¨NJbob'€ z»S~LÙ™ÝJ$Xô¼ùZîbæ™™gfgwŸÏóm«W»AÃK.Yâîçë_ýº?¦ÃëBFªf‡+´Ò/…GX8ëeIÆ”L¿@³ÛºARO’L&Ù»oûî,QRXXÈÂE † \?[uW1WÓžšãJ’D,#‹1aÂx&O©bû¶´·µ[÷ÐáZ¡ ¥cK±#ÂílYN±9+»RV«Ù'ì÷š¦»b!YñU’d¹öYâÃtûõ^µÿ“$‰«fAInœ×v&IjV&¨M›0aB1Æ£(2 Í$©[B$tŽTPWBá0²’„­mÝ>ÒLÿÀ »lzÉ—–'Cl«T܇ólx †¬àsUEU<©wUÅcõð[Î`97"3g<˜†Áñô&::{inéDU¢9!@B3Lš‰].öþœ’ÓüDÈŠB $Ý8žžölàH} ‰„æ¶Ÿ›àªª>&‘ ‡Ã–Ð… GžšV|O0” :R•æÕ€eI¹^É·«T2‚S÷‘Nw·5¡", @ 8;Éæ^•¾ÜžIöfž’%O‘À,qŽØÈ¶ïŒ¸ϺÁø o¿ó6;w¹‰¯|é+TWWcš&o¬y€¹sçrýµ×sèð!·PÀÖ­[©¯¯g„ \~ùå¾ý;ï¾CŸå†5mªeE˜0a¿ùÕoˆD"©ÎHYĆ3%ÓUµ\à·æÈ’'ë‘÷t³Œ1Üëh˜nìG__õõõ®_þâÏ-rÛžâc$ó×'ßtx,õ‘M¤0Œ0IãMêÖ …7¾Œ¹ó/äýw×bÝÝ]ôöö‡íA¬5HuÃOµd8ÜXd{>Ïs3;∔¤ÇuLv…³lgoºö•…•ƒ¼¿?ÁÎc&¦)18˜dÿÁêZ©(KIq¦ IÝ$©[;WÉzÈ£`‘$dEAV¬ÌSé×¹¯?Îá#Í´µ÷ø–—ä$Y8~€‰…5eÙpSëC¶«•×êLY=<®WjZÀ¹S¸Ò#–^pðtÝB€àìÃkñH&Ïfñ νîUûsrY±Ý¦Yb7näÑÇ¥³³Ó]öÞûïQ]]MGGÝÝV>þ¦¦&þòûICC………\zÉ¥\z饌oe¹Ú°q[¶l¡»»›ƒ5‰ÇãÌŸ7Ÿ{ï¹×íS$ñ÷3Ý“. <ý·ÜTüç•1³yñ‘~=LÓJ½{¼© ÉvC+--#d§=ÄǧT§LˆdÃŽpݹ²^’…B444_G4eÜøq4Ô7 ë==Ýäää‰ä ‡ …4ÁÀi 'ŠqžeYtÜ “i#TÉöPp„¯ãJ&Ë2²$3FV¸y®ÂE Þ= QÓb§`=ûŽÍ SRœGqQ.ѨUãDÓM4݉}U–mW¯‘Ÿ ÙßoÛŠ#)2’íê–N"¡ÑÞÑC[{í~á‘ÒX0n€)ŦeÅPV\‡ÓaÇw¸•̓V‘Á ]d0 z3])¶åÃ/>dÙoQJ·XN„ÁYCº›”$Ifö@qÓ0S®¦]$0c‡)ãwpp]´ð‹ï l÷îÝüâß~nè„ÃaV,_AõÌj«Ê7–«‹S»£¡¡P(ÄÒ%KY²d ³ªg!Ë2o¾õ&»wïæÆn$?/Ÿ>ü€H$ÂÂ… ¹îó×1gΧS©¾¥“æR•-¸7uêfʽŠìbe¤¦n†AKKªòriÙX÷øç§UIœt/2{ã$ð·…BÖ=b˜´¶´R^QNAA> õ–(îéí!//ŸÁÁ8I;å­®ëŠáfR;½ñ þûV–ewÒ %BìÁºâ´Çñ‚¶ã@ddÛmÈ ²b=&¨*wå%9Ò6È»û ŽuYÇêë[V†£Í„BŠ s))Ê%/?Š,I&$ôtNl!áÚH¥0­ösê3;ܵëëÓÖf o|‡C$`0wì3Ët‚vIUM”´DGÀ­jîŠÕ²|(²ì©óá¤Z–3,©øìnW':O‹ @ 8ãø'¤ª&;“l±ÞìL™;ô¸¥Åux™-ÛN:Ï<û º¡£ª*?øëPQQá£$fUÏbë¶­\yÅ•¬X¾I’èêêâ…_à­·ßbÒ¤IÜxÃÌœ9“ÿö½ÿFii)eee©ô¹²Õï á‘æbå}m’J›ë—;®X¦'~åŒ#œA¨óº¦k´w´»ƒÞ‚‚“ß±`H†™m,ü"Ä™ñvî}ǵ/??ßÝb0>Èà Uc#™L¢ëæ(¸`yEˆsßZ}Ï"B<î„Î É©ÎmÅfz )*²bÔSì³ SJU&%Øw<Áº“ÖÞÔy &i<ÞNãñvE¦° FQa.E…¹©@p'4ÅÌf.áe2 “®î>×Ê18˜ÌÚ.¨Ìç‚qá Šª†]!áˆ@@õ‰Ç*bY;v€¹jocÇz¨VÀy†åÃÙãYO·+ž @ =N×a‚A¦èªH`FÜA–`õt×%o@¶®é¼ýÎÛlÚ¼‰ºº:rrr¨žYÍ÷?@,#‘HpèÐ!ÀJSZQQ‘Ñ€»ïº›íÛ·c˜¯ÿéu<ˆa4k ‘HŸŸÏý÷Þï¨,Xê’óCïqz,ÞØŽtK‡Ðì¬Ï°t|Ê1Dz ‚®ë º …³»_õ÷÷£ëV±´œœ_’xÜšyˤª*G¡¦¦–ââb.œ{AÖ¾ÔÖÔqøðaJKK™5»:k›ýûöÓÐpŒñÆ3mÚTZ[ZÙ¹sápÈUž‘H€S»›‘Š +Ü@–%_ªcÃ0ˆÇãȲäKõ Ðßo¹÷EcQw™nè$“I’É„mý°¬[0z–,É>³l"ıÞXÖÃ+À Éš¦¹V¤"#+ŠT¯$Q…¤šDVæL 0£,É‘¶»M4IÄS±ÝèºAk[7­m–e8dêƒ*Á€J(¨ ª„‚öò€¼/IÖ¶‰¤F"‘$‘дŸ Í^®18˜ÌîŠåö5>7ÁÔâ$•Å&AU&` EQ]W+W„¨)k‡j§ÖµÚ*nb€×ê!gIµ›Vh2ÅÆhÄ "‚ÓKFÈFZ¸WPJoë.32->ëÇ D‡o[Ï8àøñãüã?ý#ÍÍ)—¢¾¾>ZZZøx×Çüíÿ–ÜX.@Àž1N000@8öõ;‘HP^^η¿ým~󿡯¯šÚÀ|ßtãMÜxãÄ¢1ß9¸bÁ;Ê”ü?þÞlVŽK•÷œ\Ñ‘Úiöן‚Ô ÑñõÑß>N]Ý!n¾å&®¸ò2À,ÿìŸ~N‡KóàC˹àÂ9Ô¬eժי9sÆdßÞ}¼ýö»Ì›7wH²sç.Ö´‹/ùÓ¦M¥©©™×Wý‰Â¢‚ 3lj„Èž={Ù°~ªªr×Ý·Z¢bËæ­lß¾ƒhN÷=p/ùAºU ¬÷Àq»Ò ÝÐ3R†„!Dˆiš(²‚!¥>¬j@ÅÐ û3b¹`iö Ú±‚$íA·¦(¨š5 O&“¨ŠÊ”²%‰™š44›í”èOøÏ5>˜$>„•Â{^ëàÉ *&cs4&$™Z¬ ÙâÁ¶Ü8V¬”I‰Õ$Šãál£¸Û+n–+'Ó•ßå*3ð<ýÜF!@@pz&`Ü€êEBB2³[;ì`dŠ–ô}øÖÙ«ëëë9tèK—.%‘HðÓŸý”ææfEaùË ‡Â¼ôÊK477ÓÞÞ΋/¾Èþ"U•UìÝ·Ó4ùկŷÿüÛH’5ûø3qâD®¿îz]´ˆêêjjkkéèè |R9ååå¾!¤ýÀ{b>‡ÊbåŠäTÄuœ ÎÀÔÉ–t2|øÁ‡\~ÅR$Ib÷®=®øðrÁÜ9Œ-K,7–e£ÅèÅ|ŒÄê‘ÞfîÜ ©­©£µ¥•uk?äºë?O[[;w|Œ¢(\vÙRrrrNøþ˜†eÉÒuC×íö£—+S„d¹çIeÇrÍFêž—%ÙÉ—eMב]7, MQÐ4Í!Z@CÓ¬G  1g’FõxMKÒÚcp¤ÝàH§Dc·L‹Ç(Šl¯GBn,F{{{öìeÖ¬jÖ¾¿Ž€ª¢¨ª›ý  ®öëÖ~@eU%å8p•/¿JoO/s.œC@Ͳìúx7«þ¸šþæ/˜‡9‚éîîÞøÓjÖ200@YY)Ÿ¿î&MšD<çwÿù(_üòèªÊÓOþžžžî¹ïnrsc¼öê*5rù—1eêdžûý 4551Ð?@0dÌØ.»â2JKÇ‚ kß_G]m3«gÒÕÙÅþý¸äÒ‹™=gµkØ´i ííí„B!ª&Wñ¹Å‹P™ç*Ë2W^u9/¾ð2GŽåàÁvîøI’¨ª¬dúŒinxôp˜XBÒ0 Óy=L<Õi ëäƒO„X± ¦™¢Ûß©@t M–‘uE¶ªªº"Äš¦¡é–ð°DXñA²Bƒ…¶(ë4hï3ië7è臮¸Ä`R"¡CR‡¤!‘Ô$t$B²‰ª˜“€ AÅ$4(D "ÁÓÏ€k•pƒ§á Õcñ>\âŠ]XЊIÅy¤ÄGJt WçÃ{ýG !@@ðéH‹]HÉjPã«×a -:²‹W¯Ór¯Zýúj>úè#n¼ñF^zù%t]§¸¸˜ššŽÔq›zjÐ:}út&WM¦¦¶MÓ8Z”Ù³f³ìÖe¼úÚ«˜¦Éá#‡ÝöUUU|é‹_ÊnÕðöÓHž-®#[†.¯;ÚP)wG‹O:0]¸hkßÿ€Ö~Haa!µµu,Z´ýûúHoO/ÇŽ5’——@{{¿ýÍïÐt1cÆp¸î0ÍÍ-¾}76ç±GŸÀ4MÆ••±gÏ^ÚÛ:†íO2™ä?~ýŸ´¶¶1yrÅÅElݺßýÇc<ògß ¤¤MÓimi¡¾¾’âb<À‘Ç©žUÍî]»I&’Œ[@OO73«g ih8Æ¡ºC´<ÿ¾òµ/èî¥•ÎÎõ$“IŠŠ 1 ƒýû°zÕëƒAfΜAss ;¶í §»‡n¼.kÿ‹ŠŠ˜?.›6máÝwÞGÂáW\yyZË¡m,¦m™´Ü­ÑÉ01ìÌ –k–Aª°ä°eÙpEˆ®(è²bJRCUSÂC׭׺@Óm74ݰ‹lj†I8¬S˜kPåˆ4ûv¦=ç;)=·kUIR}IJͧˆõìT'wćóZQ[¬+V€¹¬¤D‹-:O!ÉlîV^ë‡s}G[h … @ 8y†²tØcù¡\¬wP=¬;O–ù!ã:²ì¢««‹5o®à…_À4M¾ü¥/sÕ•W!IýýnÛø¬2EÅEÔÔÖ I’å>%Á½÷Ü˼¹óؾc;mmmLœ8‘Óg0cÆ §sn½²Ò3X¹«¼.f’ÇWŸÔz§` Ù“ÔŒ:Ÿ¤Ð`4cÞü¹lÞ¸™W^Z À’¥—²ÿÁa·Û´aš®STXÈû¾‹$IüËOÿ•ÆÆãn› mÀ4MÆÇùÞ·1M“Ÿüýÿ¢£#ÓÅËaÏî=´µ¶F¹é–‘%‰þþ>¶cGn¾åF&O®¢µ¥…#‡Ð×Û @^n‡¡  €d"IaQ!ùùùÖ}õÕ/Ñ××DZ†cäææR¤žþ¾~š›[˜8a¼{lY–ùòW¿Hn,†iš<õä3Ìž]ͬÙÕ Æyþù©­­£»»‡Ü!ÜÑæÍŸK]Ý!:::‘$‰¥—-±ãAFöÞH¤šº¹ÎPåáDˆŒìŠp+8Ý JwÖŽ0$Ãý\)¦&ÉÈŠŽ®éV;]µDˆ®£kššr?Ót C·â`¬e†éÞ"Þ¬|Cõ;]xxÿöŠK0xD…ý¬*–{–kápÓè*î:GŒÈN5s§&Š"Ûú–8s„š·_C½£ @ 81éAãRj°í÷xâ>²f±2Á”²»f T÷Vð*®£­½õë×ÓÓÓÃE /bÊ”)€eÉ3f ---†Á玫¯¼ÚÝß´iÓ$“IvíÚÅ{ï½Çå—_NKk Xƒã¼¼<&Mœä:äϘa o`¸÷üÜ×Ù‚ÈñX;Hóµ—RÂÃiw¶ÌRfc$©‹½,Yr)›6n¦¦¦–ªªJÆ{åCÑÙÕÀ¤ŠI®ëHEE¹O€tvZmÊ+ÊÝkV^>iXÒÚÚXÉ~ñó÷Mk‹ea©š\ņõ8rø(½=½”S5¹ŠƒRPh¥®¬²²¡†ÁïþóQššš‘nö¯þ¾>ß±«ª*¬,UöçÂ)j¹uË6¶lÙf5²×u´w )@dYæ‚ ç°ö½P*3gΰ?kCEøI¯°>Ja/C2ìDþàtÇ%Ë71áà뺎$Ë(ºŒ.+è†#4dTÃ@lK‡mùpñmAbØAùŽèpÜÓœ ¾ëFê;ÎïJ™JoëwRÒDˆÇeJUQlËbÿ-Kvߺ”û–w_N,Œ×ÕÊénžC]çÑF@  OºµÃß W‡Ã™ÁwÖùŠš)÷"_zÝÆu˜¦Éàà »víâÿüòÿ¸®<+_]É7ÜÈ÷?`Í /YÊ^úñŸ{Xn,—믿žW_}€ÿøíðü‹Ï“H$èëëC–e¾õÍoy~Ì= בíÇÞ´x3¥µwws OÊ„‰ã©¬¬àСÃ,YzɈ¶‰Ùic››RÊš<¯GÚ&ü|ËÅ+/7—-wŬišnªàʪ $êëëéì줢²‚òŠr6nØÄ®wPUU ÀžÝ{inj¡¬¬Œû¸Äû×#™Ô2ŽË͵]œþ[ñ1 /ZÀ´iSÝ{Ä0Lòòr3¶w…BT+ ïùr¿ŒD„H²†å’%I )’»NÒS™© YFV ]ÆPÛʡ躕aË40má¡ëºUhS÷Z=ôTÒÃt]¯\,F @pRÞÚâ#Í â>kˆdŽÛ½¼Žt7+Ÿ‹•,¹V'™E¶É ï½çíó™B@ d’Å"á]—­ö†‹ý»¦zF¬†ÓÞ±ž åJe˜†?æÁ„îžnV¯^ͺÖ1{öl¶oßn™ÚÁ¦«V¯b̘1\wíu>ÒØØ˜au¹ã wÐÛÓË;テattXñS§Nåû`Æt¿{Uêô²X@²X:Üëæ‰íðº`í–ŽSÉýÜK{G‡;p?s.˜Ã;o¿ÇñÆã<õÄ3¨ªÂáCG2Ú¬_¿‘Cu‡yö™çHjÇ7 »ß™3gÍ¡»§‡>øéÓ§Ñß?Àþý¨¨(ç’’bÂá0ãÇ£¡áí\~ÅRÊ+&ÐÜÜlYZœz0öÛ×××ÇþýØ¿ï€{/žˆêYÕ¬[û{÷î'‹EikkgïÞ},_qF{I’…ÂÙEñ(fð:]xÅFj83ù®åÕqQTd;vÌ@2$;hÝ0eË•J20$ËUKV CqE‡a?+ŠU´Ñu»ÒýâÃ0ÍT‘FG¬º¤lç`?x\ dIòeì’$ɲhx-éE}ñ")ë‰?¾#UÏÃ`žþ]ãp6‰D i ™*}½û”ÖÞ6vø‚ÌM3«µc¨¸Žlƒ€×^{ ÝÐyï½÷ˆF£üè¿ÿˆŠŠ ^{í5žù½åSÿò+/sõUW3vìX¦MÆþûiii¡¦¦†©S§ºû ƒ|õ+_å†ëo ¶¶UU©ªªbüøñCºW¥‹Ç=ÄÛW×Ú“ÅÊáú·gDž¿RXT8âöå哸éæYýÇ×Ù¶m;&Œgþ‚ylÞ¼Åm3cæt®¾æ*Þ~ë6mÚBUU%^8‡;>r¿ÑX”ZÎÊ—_cÛ–ílÛ²“ÂÂB,˜ç¶«ªª¢¡ÁÊ’6©¼œh4JII1-­­”+# cš&3gÎàã)»©«­ã¯®fÁÂyÄb¹ôôô }r¶VX¸p>ÉD‚­[·±ví(²LÙ¸² ‹€"+VñÁ´Yì󌔹µÌ¶x˜†dņ8‚Þ0$$Ûúj˜– H¹R¶¥Kψñp²¥ê¡Øâà †°oÚÇÇó½–Þ?$ëC–e[0ɶ±Å‚â CXG\«‰2dl‰SÓ#Ýúj=;.y™×ñL#>Üe”kC’Îó»Z  PÙt“íì H2’½Î÷e˜ý gŽp‹¼_÷Rª;‘šMp¤o‡'É 1S§û—{ÝöîÝËÚµk¹háE,X°À¸~LÿüÓfë¶­,\°ï~÷»HXn?øáÜ U?ü›2uêTÞ~çm~û»ßpíç¯åá‡N]taö{}¢Ô¹Îkï̬랑ÖÞkõ8WpÞ]·*gÇãt÷ôð§5«éh·b)®»áZÀ'*È6‰ýýäÙ&$‘t3h9}úa­Çãtwu“Í!Ÿ`oÌ„™Ú¯úú‘™@ 0²ý8é¦ kàÛÓÓƒa˜äääØq©¶NïÌ{z ‡õV™ž6¦m%PXxÑ|4MãO«ß°ßOƒâ¢JKK)--£¤¸„üü˜2²ìˆ ٴܲL9‹81RL¯@4I‰N Gä Õkç{Á XîXö2GP áËV% ÷:Mpx·unÌ Ƴñ»G@ 8ÏȘi7ÓÜœø 35`ÎâáüÐe °ÌÌàQ_°9iV{ßÕÕÕäççÓÕÕÅŽ;øçŸý3;vì@Ó4þñ'ÿÈ„ñ|3šNÑÀk®¾†… ’É¡ Ÿºº:jëj©¬¨D’$€{žV¶¢h4ÊâE‹Ñ4Ë.»ÌEü$qÖ)Ûƒ9ÕÞ7ðI1œ?þŸ]N+q"±qJtÀ vdUÁ¸V–¡wpîÇ~œˆ¨ÕÖ!¤R‚›¦Œ¬¤¬–1ÃfƳéX4«SÊÜ”!BÜî8“â¤ãrj¹“rÛqÍLÎpµr„‡½ýp×îlE@ 8Ÿ13Ÿ}.Bör+Ÿ~v¨Œ˜Óï ád†Ép]2SvHÍÒÕÖÕ’——GWW†a°eË$IböìÙ$ LÓ¤¡±uk×±îƒu´µµ¸äâKˆD",Z´È ÿå/ɲeËèééá5–‹I4eú´én_þì[æt,Õ—!Ü«œ~:Ï>ë…äq#s„ˆ™<¤ïGpn3ªBÄ~VÕ®‚ž}ÐýYe¨˜0ïºôe>!‚é›Ô0eÓpbÔÛ…0%8R…QS.o¶ùƒ½7nì…ä ©¶]AkõÈ\çYæ9‡…‡ƒ @pžád Óð ÌH{kúc2Ò]ªÌÔ+Ã.–!t<èºUlå«+9zGo;é IDATô¨»ÒÇ<®XIk—L&Y¿a=›·l¦®®Žd2É¿þË¿rÍU×PVZÆ–­[hll¤««‹x<îûá\réÞXó—^r)—^z©ëf0sÆLJJJhmmàûõ}Ž>B4eÎì9¾lKCÅu8¯½?òÞ¸ÿe4ý¢ãÜø}?oI&“<÷û¸÷þ»ÝÚ#cäBäTí·lÙFaaå¾åŠ¢¢†¬˜$kÖýô{VáNˆx×; •L"]¸ûâ{ïÄ.XÞש/^ß÷Ï0Ã]6Œ›U¶ó;—øl%"‚ó¯åÁ#6\k‡‘šá3 ÿÎô§|Û¸ÿÒ,&CýðIHìܹ“_þê—tww3þ|þòÿýK~ó«ßð»ÿüsfÏq÷³äÒ%î~>øàßñÃá°›­*ã’ÄeK/sÈÚuk3„•û/¥¶Iq1L#%¸¤ÔrI²â:|™fÎÝßùó MÓxç÷XzÙßrÓ4ioïàøñ¦¬)ûûú­"…¦‰¦i$“Iw»x<À@?MžB†‰ÁÇÝõéÇ;~¼‰Ö–ÖŒÁh__?Çéïp—Mœ8Y’h8Ú@8AUçôàñlÃù|ù½d¯Ë¨¡áYîKë,“d» ¿ ¹bW/Ïöð®OßÎ_ãC¶3]eöåD®V#qÅ:Û@ 8Çq,ÎL½wÏgÕ03cÜAR©>½q!é±!Þí’É$ÇŽ£°°ÐWƒaëÖ­˜¦I$á;þ‰µuµ475ɉ0Þ|………TϬf×î]´´¶ðëÿø5áP˜Í›7óãÿùc7“•ƒëN%Á¸ñã˜2e 5µ54oäàÁƒL›6-Õ6ˬ¢›ÊTòÌnZ;vÛf³šÎÖû'9nõÔÕ" 2n\™Û¦££“Ç}’ÖÖV"‘0˜ðÐWPVV†‰Éûï­eÕk«)-+e0gÜøñäæF¹ù–›hiiá—ÿþ+n1Û¶ncü„ñÜÿÀ½¬}o¿ý®åê×ÒÊ¥K/áòË—°mÛvÞw-…$I‚¡ ÷Þw7š¦ñÔÏ088Hn^.m­mÜrëÍTTV Ë3fÎ`ÏÞ}L™65Ë™Žèj ¬'æd¬"ÃgØÊt·:ÙAê{ˆŒ·O’R31eYº~® Žt„‚s)íG2= ¥µ_ŸË–Gt¦ASKù¹ù„#á¬Û8x€ç_xž={ö¸³Í“«&ó­o~‹²²2*++àkßøš¦ùú[YQÉþûP…e·.c÷žÝ¬]»Öm³yËf®¹ú÷<2â8LXºt)---\rñ%®Jÿ!wÒ;3†N`¹wŸÙУKƬtzìŽMsS cJÇøÜ¯^|þL˜8žï|÷ÏP…5o¼ÅóϽÈùîwhijâõU¯óÈ7¿Î¤ŠI4?Îÿþù¿³hñBë¸`[CLþë_|Y–9x †÷ß[ÇŸû›äççÓÓÓÿÿâ—T”—S^1‰÷ß[ÇwÝθqe˜¦Éàà {vï%'åË_ù¢ûÙÓ4 E±Üz ‹ 9°ÿÀ .£çvž“nñI;¿xùô_¢AÈ)ç'B€Á¹Š3›Ÿ·‘1˜E|8Ë ´¤Æ–­[øðÃÙ½{7}ý}Ì™3‡¯}õk»ªU«WñäSOft¥¶®–ýÏñOÿ럸âò+ص{|ð+>rssÑ4>ÄŽ;X0sæÌáÎ;îdÍ›kÈåR=«š%—.aÚÔiCÇtØç}õUWóùk>ªªN÷‡L_™æS=ññIf>'&ån"3ÒËÛÔÔĘ’÷ïx<ξ½û¹ûÞ;©«­Ã0M h¨?F2™¤¶¦Ž1cÇ2eê0M&N˜HUU%Ь¸q‹/r+ºïÞ½‡I“&ÒÙÙE{[†a0¶t GŽ¡²ª‚‚ü|Ö´ æSYUIn,”””ðþûëØ¼y+3¦O#7/UUÝÏgaa!íí'{•JäÓ“-ädÚµìdIÏ x:Žq® ˆ@ œ«x\‡²‹§ãuA²Z»Y»n-7nd`` £ýÇÌ¿ýÛ¿ñÃüÓ4©©­áé§Ÿ ¼¼œÿú½ÿJ<ç§?ý)Í-ÍôööòÒK/±bù ¾þÕ¯síç¯Å4LJKKÉËËc×î]üÃOþ€¶¶6÷8·ávî¸ýÿy¥eîò®s~¤½²só ÷ºxf•3‡ç:¦],w¿Ÿ¥AÁéæ“ˆ/¡`D"áþÝÛÛ ÀáÃG¨?Úà.¿äÒ‹Ñ5øÀ…i®|…àøø+*ÁPˆÒÒR[œ›Äâ$ öí݇›Ë½L×tþõÿ«;€‹D",ºh“'O¦©©‰U«W°ÿÀ~öìÙCuu5[·nu]šî½û^Š‹Š11ùÆ×¾ÁÿîÇ Á† XþÀrdYfꔩH’D__›6mâÙ矬Z ,°úï‰ëðâdq)K³vxØ}f<ˇ+>˜ÁPîY‚O„$É(²|¬>'¢l\[6ouÿ.,,DUf̜μys}mMJÆ”ðÖ›oc²}OÕÕÔ1sÖLOçüÇ(SBoo/Ën»ÕviL¹6š¦I4åŠ+/çò+.ãÀƒ¼ðü¸à‚ e™ÉS&S5¹ŠÄà Ï=û;v|ÌÅ—, £½ƒâ⢟¤+˜%D^ÞÑc81ðIÅÉgY`œ!@àÄ›n× @OÏeïàô,WU•‹?w1kÞ\ÀÂ… ùÆ×¿á¶«­­eßþ}>r˜ê™Õ´w´»Ûwvvº‡é3¦ËÑÓÓC{G;}ý}Ä¢1~üw?æøñãtww»Çž0a|ãJŠK†v±òX,Ò-îkÇ¢‘PjgËúÃ/Æ£N*»Ð©ÙßĉXýÇ×]á©( —_qZõ¦a2}Æ4º»º©©©eÉÒ%Ì™3›Õy«yüÑ'X´h!Û·ïDÓµañ¹Ï-æ£׳æ7™;÷BÁ ‡¡¨¸ˆ¢¢"¶mÙÊŒÓ †‚tvvRXT€¢(ìÛ·Ÿp8ÄØ±cééée``€’’bw¿­­­Œ;öäOZ‘3ާ!@à\Äã~å”{óÊgXH Ó7x71Y²t‰+@6mÚDüá8Á`Ã0L º‡r*”WVTòï 7mäòË/w‚ùôöö GˆæD˜1}`ìØ±TϬfá‚…Ì_0ß/²ˆß¹‘:w;OÜ‹“½Ëm‹?æCˆŽ3é'M¤dL {vïeÖìjn¼ùrssy÷÷x奕pÁs«Âø7ÿ웬yc ›6maæÌ$“ ¢¶[T0 ÊNšàPXXÀ7ùkּɣ¿{UU)7Ž«®¾Y–¨¯¯gýúèºNeU·.»C×yû­wéhï ¨¨Ù³g1mÚTLLt]gïž}Ü¿üÞžééw¹ƒjÁ™D@ 8Çqà&˜’é·˜¦‰,É®µÄëÂ4mê4Æ–Ž¥¹©™þþ~>ZÿEEEüá¥?pèÐ!Àò}Ÿ7wóçÍç©§Ÿ"™L²uÛVV­^Å¢E‹Ø¸q#õ õ,X°ÀØÜqûÜuç]¾ŒEn&ª,îP>÷Ǻ#{Fšðp¶“¥!JZ‰ñÕ)'ÝÝ-Ûú¡ê·œ*®½îÞ~ë]W€È²ÌåW^ÆåW^ækçÜê±X”/Ü~˜&¼òÊJ,´\ ùâ—ΰ–••²|ÅýY]°î¸óv »ÍÌê™Ì˜9Ã%ñ~ÞöìÞKyÅ$ŠŠŠNÎç4è¯Hw>WBŒF!@à< }Pãµ8ŽºA?óçÎçÕ×^E–e~õë_ùÚL™<…ï|û;ƒALÓ¤¸¸˜ë®½Ž?®ú#O=ýO=ý”Û¾¼¼œ/éËî`Æpë éµ6²åå—$É$öÌ-^K‡£Ë‰ £É²‚ü)cUaÜŽ×HuÌv›’S‚Ã[—íÏaã̾ºñÞ tÁǺͬ÷°° Ð]Þ|¼™‰'œ¥ƒz›ÓÏ=ºŒð|$Ir³puvvÖçNQ(Šâ>¬d²+ú…D0"ç ]]]¼öÚk¬Z½Ššƒ5äççSXXHaa!ƒ‰A‰„ûHjI’Z$Xºt)+_]‰®ëÔ××s´þ(“&N²vê5<¨Üå¶WŽ›‹Ìøg[¯¥ÃÚÜÓáðâ>%²l˜¦jY?~œD"I0hUwêY‰WÔžU‚äôt¨l\9Ñh<ÖXù€ŠªZE±\ç„èŒ6gñ7…@ ND"‘`åÊ•<ôÐCÌž3›¿ú«¿bÓ¦M$“I’É$‰D‚ÁÁA÷‘H$$‰1%c˜2y Œ+ǼyóÜ}®û`ûÚ >„8ðÌœÊX)ÕÎ(^‹Šå{n­ÏºÁG²]s •VW‘rr¢Ú"Ä4M6®ßè¾6 ãLu÷³AÖÏHê}r(-+eÂÄñlߺ]×0 ƒ@ @("PÕç’%ŒÂ"ç }ôO=õÏ?ÿ<¨ªJ0$ £i–{•×â¡i•••,X°€ÊÊÊ”UÂŽ»¸léeôôô°tÉR.¾øb e•ÈæßobZ""ÍMÊW$m,ã¦àM·zÎZLÓŸ0@Vd•9³/àƒ×¢ë:Ô¬eÊÔÉ®ÉH2p¶pÖY?>!Y¬9¡pˆœhcƔœûÐÞnÕt€  ……‚–DURâï%œN„‚s„ºº:üqž|òI8àËdc†a ë:º¦[Öd‚)“§pÃ7°dÉrrrÜÁŠ3À00X´h‹-²‰7˜Û­Ê#4œš#Èþ¬\^¡’-6ı–X@X=ΜøY–Q5  ¿ÊŠÉÔÔ`ïž½´¶´°à¢³nk%zÊRÃÄ-›ó ûë|ž>ªñ•îùû1Sâ¬Ïã`<Îöí;hmiu›ºA$–C$’C$!Ž S®Xöõq–=àS ˆ@ |J^zùe~ýë_ñüsωDNé¾;;;yöÙgyâ‰'X»v­o·8š3ó¬:ãÇç¾ûîcÙmËÜX«8š5P1LËMÆéƒlu9Ò‹•ù‚ß³Ív{ÄJÆ OŒmÎ9dÙ Š¢ …CL®šLggmíÖ ·µµ7ßx‹ŠŠrJÆ”—oeZ;«³®¡îö1=öéÓ`fûsä‚d €®®.:;;9|蚦¹ë‰$±h.±XŒÜÜ\¢9Q¡Á %@„ –`´D >ÿùÛßòƒþ€×þøGîºóÎO½Ïd2ɪU«xüñÇyõÕWÝzÑŸŸÏ=÷ÜÃý÷ßÏâÅ‹ý– '3•ÇÅb¨G6±á#énT^+‘F÷Ü'u¯HÔÔÔðècrÃõ70mú4"99Ìš5‡C‡ji8VïZàjk먭­°³, W»ÓsíÓÑu]7‰Fcäå呟Ÿo XŒp8L0$` `4D >!ÿôÏÿÌÏþs òrÑtG}ôS õë×óÄOðÌ3ÏÐÖÖvÂöªªróÍ7óàƒrÓM7 …ü.!ÿp¯è0M3U£ƒTŒG¶ø §€¡Ý03uIš°½ „è8çéêêâ÷¿ÿ=O?ý4EEE,_¾œùóç“L&Ñ#V¬Qyy‘H”úúÃôôû¶·À™cÁéÅ4M4Ý@UTr£Qb¹Qòó ((( //œH„p8L °+ØËÙ-™ÁiB@ 8ILÓ䯾ÿ}ž~æiòòbH’„*©|´~=MMM”––Žx_‡â‰'žà‰'ž`ÿþý#Úæâ‹/fÅŠÜ{ï½ûû6DA8¯åà —SèŽ+–/Õ®Nž±ÜÆ›šW23Óï ÎMt]ç7Þà±Ç£¡¡»ï¾›çž{Ž¢¢" Ã@Ó47îH7 °ï¡P8DOw7ÝÝ]ôõ÷£iIt]·] ÜÛç,‹9/pÈM•`@%š$ ͉‹ÅÈËË'77Fn,œœB!K€8ñÁh"ˆ@ œº®óÈ7¿Éš7׋æ KH¦ ùyüþÙgùη¿=ì>:;;yî¹ç܏ޑìVUU±bÅ V¬XÁ´iÓü+³s»E m+H¶*ÇÞJä¾užº>W,9Ó-+cÁ9Ë®]»xì±Çxë­·¸æškø›¿ùfÍšø³›)ª‚‰IÈY˰Ô"á0Ñh”x<î¦}ÖuÝÐ1t0]ýq¶ªŸ‹¤W©—e«¸ “j7“ŽF‰æD‰Fs\ë‡ã"ç-0*ŒÒáÃ]&@Ù¸6$I|>«H€ÈH’ýŒl-“d${ï5Êì¯àLÇYþàƒlÛ¶•p(dM:z‚±u]'ÍeÓÆÛ&“IV¯^ÍO<Á+¯¼2¢¸Ž‚‚î¾ûn|ðA–,YrÂÁ;Ht"k‡èyÛ9Y°¼çamî±hx3`¥¢vçmmm<ýôÓ<ûì³Lœ8‘‡zˆk¯½EÉü~s3§Ù×4-I2©1˜d0'Ó?0`‹¸Uð2‘DÓu ]Ç0 Ÿ àÔàX(LeŽ ¬”»áP˜p$b=‡C„Ba7ø\UUßöÁéÄ4¡¾¾@ ===Üq×]ÔÕÕ Û3¿iÚŠ"SßÐÀÇ»v1göl6lØàÆu´¶¶fÝ·—@ À7ÞÈŠ+¸õÖ[­¸Žâ O‹qc<Ñd¯—%9•Ë0dëGÂ7@4SÛ˜’/g¨`ĘÖ4õ×n4N’ƒÇ{Œöövî»ï>V®\I~~þ°Û9÷W*^ÀD’¬×Ь ª‚Á‰H‚¤fÁ4tM×0 Ó01L#{‚Á'²^ÚX’l EQ ¨* Ul0´2^9ÂÄ—zWú¾NÂ"Â"Ž––n]¶ŒööV;h3{ÆÓ4L$(.*aêÔ©lܰŒè‹/fÅŠÜwß}”””œšŽ{,Ž»U¶uÎzg™/ðaõ8ÕdË06ÔúSɶmÛxôÑGY·n×_==ôP¦;ßH·„¦®énLç¡ëšeý°Å‡ã~õ©jf²b•9‘S®XŠ‚ªX¢Ð²tX¯EqÝ®¼VaýŒÂ"#äÈÑ£Ü|Ë- ƆÁ@€¦¦FjjkˆÛ¶²²’+V°|ùrf̘q*»m!ùã<»è™ícXµC܊禙Zçq•ƒ“OOºè¤T•qI–2Ýà85ƒÂ¦¦&ž|òI^xá¦NÊÃ?ÌOúÓS²og+›2²d¹þ¨ªê¦„5LÃNº•$A¸`* ž{I–$7³•U¿EñÅz8®ZÞ}£ @0{÷îå–[oEQdA5£æE:Ž‹J4%‰Ðx¼ UU}Áòóó¹ûî»Y±b—]vÙ©ýñÏfÕðôK7u$Sr³º%844;@Ø Ê’d ÅLõ©ÃSÅyϽ³ÐÞ×Þ¬cÙîÃ08zô(Y588ÈÊ•+yüñÇààõ×_'‹¢Sñ¡ôž“ÞÙ)Œéµ¼yE‡ §† kšäÜcŽE$û½•m{`4D ²°yófn¿óNr"aU>¡øpd ÉMi+ƒ$¡ª*7Üp+V¬`Ù²e„ÃáÓÓé,âR®2Vm Ó4Ð5{–C·×“Šï0 3ï!Æ(ŸoJdg€({†ŽÏþP³ÔéÖþþ~–/_ÎÃ?œ!@6lØÀc=ÆÆ¹ùæ›ùùÏNeeåé;7Ojfo%¯à°^§n"!Ó˜¦É÷ÿú¯yò©§ÈË‹Q\\LeE%ºaÅDFšàÐíºaâ´¤ßXÊ’D~^/¼ø"|ã£r&¦ëz¥iƒ‰ï¼ø¾+>ÂÁ 3ª*ƒ§½O‚LòíLT†ap´±‰Æ6«8eËá6Ö¯ÜÌâeóÝ„¿ûÝïøÙÏ~ƸqãÈÍÍõ¨K’Ä“O>Éœ9s¸á†Îä) ÁI#\°@¸`}6ÑuG¾õ-Þxã b±¦M›Jey¥U¿À+4 ÝÐÝJÎ>aâiç,kkk£££]7ÈÏ/à£?<íç’pžH$¨ÝsˆßÿÓ v8˜X:–ñ¥c}¶Ž3ív•q|§N‰é_ï¦63·u÷á­qâÙGƾ¼Ñv»Œý§ÌËv¬áŽ™íX²,„#V&´Þ¾~ö:LR·âqf^5•)s«((( 77×JéŽ ZîXÞlS@p.!\°Ágžd2ÉýË—óÁ‡ë(*(@U8ÈÁíÌDVv¢@ UAØÊN^É IDAT¤¨vá5Éž•T$)ˆ,IäææRTTDMM ‡fÿþýLŸ>ý´‹w°ë¸„Åâ¼þèW|ä1¡tlæ6žÿÏÞ¿O 1÷sJ€˜îCìcˆcžT?FØ>mY| Žiš„#a Š ˆEs˜V1‰Ý5u ÁÁuuŒËEQ¬ Y·’µjó“„øçúúúèéé¡«»›žî.z{ûèéíuÛô÷$>8È;uêéı~8.X 5Çœ5Lž8Ñuݱ–¤þÿl2¼ˆR¨pꮚ¬È¨A…øÑœ(ãÊhmïÀ0Mºš{èëë#É!'’C8¶ÓãŒ»Ì Á©@@ °Q…¼¼<òòòNé~Oç 1½è Sl°éH‹µˆåDSmÜ¥ç(£Øýl$Õ‘Os5ÃÑ0HIMCÓ4TU%'¦·€Á®ýýýÄã &IjIt]Ã4ƒV¢aýç8ò‰›àÓ0ZƒEÓ´Ý¿4¶†vwy0àk:‡ÅÇ(ÓÜÕÌæš­l¯ÛáYjz~†^ã'"+©$šf GíB‚zÜ ¯»ŸÁÁA‰º¦¡ÙéŸM#%@„5D œ«"ç– –58M$ ö ÅlyÒkž4u6³¥f+Ûí8qã̃dEQ‚ d]·j|ä„S•Ìã½q‰A’ÉIMðë̸V!>Á9Œ @pÞ`º©¿eøCÖ3‹$Ë„ca2Õ‰õÎÈJj¹¦ihšåNç¿4MS¼‰à¼@Ä€Á9Ž[„ÐNkV®ìNBÙY¿g}]L3‰p0Ä®C»)ˆpÙK0M“us¤ù(}ñ~ò£yÌr!ãŠÊÜíߨü&†i0»b-]-Ô5" ³`Ú<_»„–`óþ­4u4‘ÐäGó™;ùJ J8ÖÖÈLJvùÎÍê¹ÉU^¢(è†Áκ4´c`pÀÚGÕ”ä• y~‡›p°±†X8Êâi‹øhßúýÌ«šKA4Ÿ†¶ch8H(äâéŸómßïekív:ú:›WÂÂÉ P”ÔOè±öcìoa:Ë/»Ÿk/ü<m½íì;¶ŸPN5 p28)•­ìfÖ2 x.΄D ΜT¼CF@€’¼b.™•r?rÜ¡EáÍ-ï–@<1HgoÅyEnû)ã'SË ²´‚]‡÷ÐÒi¥nêh`|ñ8J -—«™å38Ôt˜x"Ng_'…ÑBdY¦³§“×Ö¯"‘¤º|&WÏ»Lhîl¶ú#+¬ÙbÕq‰'ãôÅûè‹÷ E3ÎK‘Ê Ëhh;FcG# -‰"+L.«âPÓazã}tõwðÿ³÷ÞQr\÷ïçÞ 'ç ƒ’0Š¢¨,S²žlš –e˲¬gï³×ëÝwöyŸÏ±÷½Ýõúx½²õ¼ÞõJ²WI›IS$  ’ ‘ˆ0ˆƒ09Ïtw…ûþ¨ÐÝ3`‰÷sÎLwWݺu«»ºë~ë—ÚêÚflâµØ¦MSU#]'Ò;Óàøž bnV´.Ã4LÚëMfÏ345Œ•°Îé3˜‰<×h4×Z€h4Í5Æ…LT[êšË^û~0¹–Bb†©cM#Å¢¦…á¾ü²öfI\Dô<êÃÇeÈ’6%Ͻ°ÝÈÄ(¿üÏLä'Y¾`Ywwq<áþ„áx22C¦!öQn‘)eA}k @O‘w 4U7² ¾ý'°ãðN²É UéÊ2#’mÙ¤ìTÙ1Eïqtl†(Z9 i aœŸTª((‹ÂR£Ñh® ´Ñh4MŒåž¹õUA`·ã:ܽî.ª2uÃó=Žô×G:u˜ Ë×áû>GzÄ’úª:N œäÄÀ ƧÆI'Ó8Ñ€iZÔd«›ã‘—ÿ™ñÜK[ó±  ·CEP_YÇ^öãùY÷aÒ‰J)\Ï¥»ï8•¡xxöíçbYëRÖ-¾ˆ,Û85Ôƒ§<Ö/YGkM »í Û”»_šlM\ñPïaj³µ Ž2^GbÆû£Ñh4-@4FsÖ,ZÅ;]ïÒ?:À÷Ÿ}€¶ú6”òéîCÁoßóµ²öÃãÃ|ûÉ¿ÇW>NX`oÃòõܼr#{í#ïøîÓÿ‹T"ÅøÔ8·­ºC¼u`{¤~´ï÷äwÞ¿ñ±¯p}çZvÞÅèäßþÚêZp<—þ‘~RvŠŽÐ2321ÂÀè-5E‹NcU¶eSp €¢¥¦™l*KEª"Þg[í¹ l2ÃêöÕì:º›wï ëÔAr^!ƒùë:VŸSFó~@ F£ÑÐT݈)Mª³ÕeË ipß]¿Ì¶}or¤÷}#}ئMK]3‹[:gôsÛš[æð©#$ì7-_‹‚l*Ë—?òE^ݽ•ž¡^ Nõ ¸qÉõ,m]‚BQ­bA}kìUt@ÑÆ¡¬h[Îñ€*‹ÿÈ$3´Ô4c™ÅŽ” EMѵê–囨ÉTÓÕs g’ºl-µÍlZ~S°­öžÒh4š2Ä‘##  ¹e!ô¯¤æýŠ @"Døˆ – ‰ו=çܲÚh4óAày…BžÉ©)ûùûw? E*‘à†ËbüÄ|ð×þ žïó‘õw³¶sÍœ·‹cÂ&¥¥ëg]6íñ¬}”ÄQÌeÛ3ö­ ÛY ;¬p^¶â4}”? Ë0Éd2ô Ñu´€ªëÓ´v6ÑÜÜJSS#µ5µTVV‘L%±L+ŽÑUî5ÍÕ‚RÐݦa¿ÌcÑh4æªEJ‰¶1Ì@èÛxFsv´Ñh4ÍÕ⨯¬»¸GÞglR R¿”XI ;aÏiŒF£)¢ˆF£Ñh.˜;¯¿ãrá’a˜v*0dÑÝJ£Ñh4sF F£Ñhæ€4$VÒÆ° JB;´D£ÑhæŒ F£¹l\.÷©sAš¦mbZæ?VF£¹go¢Ñh4ÍÜb×áÝÞÑeJî48>Ä“0m“TEŠT6…ißý:q¹D£Ñh®`´Ñh4ÍEã•][)¸…Ë=Œó¦2SÁÖý?‡¤ ‘N" n[£Ñh.6Z€h4æ‚B041̱¾c¬]rÒHC"¤¸âkUAå6éÊ4U5U,k_Æöïjë…F£ÑÌ:D£Ñh4sFH‹ )K†ìØý2k–®¦¢ª²l›Ý]ì>´‡ñÉ1šj›¸eÍÍ$ÌDPDÑóÙwl?;î`傼RHVµ¯à…í?cmçºNâÄÀ 6,[O{ÃNžbב݌LŒR“­æ¦eȦ2¸žËÞcû8Öœ‰Ü•é Ö/YGM¶¤`߉ý8Îxn‚ªL%7-ß@]¢\·h5lyŒ›WmÄÚ¢Ñh4mÑh4ÍiB`X&v:A²"E2›ÂN%0m i)!´pœè;AcMcÙöoìy“§·n¦¶²† «600:ÀƒÏýÂX ‹#ýÇxúÍg©©®áú׳³{7Û¼EïXV´LößÇSÛ63<1ÂÚÎëȦ²t<Èã?‚¤dÝ’(¸Úò#r…{޽Çîcï±bá2n_{+ šÚÀ†tU†C‡ØydËÛ—qÛu·ÐZ×oPWY‡ã: Ž^º7Z£ÑhÞGh ˆF£ÑhÊD‡aHsn¥ƒ£ƒÔTÔÄË×áåw^áî wqòëXÒ¶˜oýð¿±ïè~V-ZÉÖݯ³rár>~óÇP(–µ/å¯úÿ0 +i¡P¦A[S+÷Üö)|_ñÔOžaýÊuÜqýíàꥫø‡§¾Ç¾ƒlZµ‘ã#'Yѹ‚5K× PtÒI”;÷à‰Ã,nídyû2”R´7,W® à"•e`t†ê†‹ûæj4F F£ÑH)1mÃ68×4TãS㸾GE:/¤àèîíæÔ`O¼¼‡w»vðò»¯ð™;î¡­®•e –ÒZ×Âî#ï±ëð^Ùù÷Üò)4¶ÅûÏS‘®8ûë„FsÎh¢Ñh4ïS¢ÊÞÒ¸ðp@)$µ•µ ±¨¥b)¤øÊgÕ¢•³n×PUÏ‘SG¸~ÉZ pÛ:1p2(³‘MeHÚI nž• WݧJƒvY6­ÚÈÆU7ñÔÖ§Ùqpmuu%LsÓŠõlX¾Žg·=ÏŽC;câùcSãÔUž~ ‚Ö,æýŽ Fsq¶´·B ¬¤qÙh©k¢o¸?~L$¹yÍ&^ÝñùBž¥íKñ<—C'ÓÙÒISm#·®½…ýôà 6·óöÞíTÎÁòðîàÅ·_B ƒå –‚ï;NU¶ŠEMly÷e6.¤¶²†¡ñaN õrËê¼²ó5Úê[©«¬ed|”Sƒ=ܸô†¸ïÁÑALiP;Od.‰ÅDü§Ñh4×Z€\B”º8÷¼ôÅH£ÑÌFq²:ûo3aaÚóóÓÓª›x`ó?r×úb›wÞøª2UìèÚÉ›ï½E*™¢µ¾•tg €Î–EÜ{÷çØq`'ÇzºùÀwðó¯“N¦ã~ÛÚHÚɲ}]¿ä:R‰oí}›]‡va[6Õ´5V )$/ïx…ñ© ê«ê¸~ñuµ0¤Ák»¶2>5AME «:V°º£h¡Ùuxk­Á2¬‹ö»='Jw%‚"\®…ˆF£¹–Ð两¦ د¾hi4×>ñ÷\”Fìçã¸nÜ&™MÆ)s烆êzÚ°³k'ëW¬‹—ß°ìú8 Ölt4wÐÑÔB1‘›äxßqnZ¹>^ï‡>WæZ±´m KÛ–Ìê‚uûÚÛ¸íº[bv«(Zý–Õ›¸yU` ‰OÃm NýÝûùÂÝŸŸu¬QÒßÅAŠÀ.ï8ÅeR"eP_EÈè3Sú÷\£Ñ\hr.¥ÈˆöuÁ˜i7@Ït úb¦Ñ\+( ˜¬¦¤¢6ËèÀ®çáù>†”(5¯ú€;o¼ƒî¾ãsn?:1Êã¯<Á¦vÆ&ÇÙwl?íMí,]°dGy†ñLŽqÇÚÛゆ§å"rFà 715÷m¦P„H¤áoµˆ­!Fs5£H "6δí¹v;×qœö¤¢õg¿HͶ/}qÓh®.¢É©Áz) j[ªC¹|žL*…ïúaŠÝù£¾ºžúêú9·Ï¦²lX±ž¾á>*ÒY>qËÇXÖ¶tÞ…Ò騯ª ²x]¢{OBL+¸O†D&¦e"¥aÅ¥”Ú K£Ñ\3¼¯Èé&úBÌ ³µ±è4wÄæÏŠ2Û…HÅË‹•Y¶<ÃEL‹æê$š J)¨k«ãÈÎn@qª€%í p Ò”%.=—)%+:–³¢cy™;Ôü(€+/ÿTÒJ ¥dlb’|!pÁ’‰a˜¦‰a%qùÜ5æbñ¾ sýÑžžƒ>Þì ®Me}‹h#1ûú‹€¢¬ÏrÑ4ÝO¸ôy0®ÙÄÉt‘QÚçôñkA¢Ñ\YÄâÃHi°`y+om~ô ÑX[KE&›s0S–þ_Ø–E"™À÷}=/OԚضüYV,D„Š™±´Ñh4W+×´™ës¹8ýz “™V†¸øÑ5c 3Æ4—9@©¶‰' ³Ä‹ˆbféòÙ„Ål˜.JÔt_ãV.~ôDF£¹ì"ˆ0-“†¶z–¬[Ä·°÷ÐaÖ­2=&ò˜ Ôór‘˜àïó§üwxnkІmÛXV)ìð‰“ä d¥"Û”!‘HH$±mӌܱdø9âCÿöj4š«•kN€Ì.:f·FÌfÙ(åÖ¥Tض(.Šû+o[Þ¾tGf ) BÌq NQ¬¨ŠË§µ]H”¿W廜yÁÓbD£¹|Ä“Ñ0DJ‰iX–Åú]ω®SLŽLáúoïÙËÊEdCKˆKP$pɺœß]UöP¾fÖ…³4nÀL»ÝRò?jVÞB‰3ßšýçzºîl-Ô¬ë#K€ëºt;ÎÐèh¸R‘Y’$•J‘IgH¥’؉¦eaF®Ñh4W;ׄ9Û„^•øÇžQjšecú2 ¿äµšæ’TÅ~Ã'3DËL«IÙ§]ÁÄ,“â#~*¢ñq‰™¡¥$hœö:8f1Muzßåïµ#Íå øþc,Ë"]‘á¦OÝÀ+½çy¸®ËÎ]4ÔÖÐÚÐ@2aƒ_jݽ¶³<›ÙFœ¹É<üŒ‡á±qŽž<…[š&y¡IEu–l6K&“!•J“L$c7,ˆ®Ñh®®j2«;Ó KFÜšX,LªÄB¡”*æ“W~™ ‰…Œ*·)ícšð˜aE)Ü™™ElÌfé(.¡J ’EB#("Ú^/¾A·¥UwýYIt·µøk1¢Ñ\9¼óÎv¾ÿýðî»ïòÀL&i[ÖÆ­Ÿ÷ÙþÔÆ§‚˜Á!ú‡BNÜËjyŸàû>“S¹²Ú,Xê4¨n­¢²¢’ÊÊJ***ÈdÒ$6–e‡–Éô›JFs5rÕ 3Z;Ôl£Tdø¨X0”¶Wñò¸}´Þ÷‹ëE$>()‘`Q(%JžGCR%Öx˜¥ƒ;ó—ÆrÄ‹ÊÍ‘ëa€bЦ(<ÊÄ…,'E!",zõWtÝš.HŠc(ŽjV12‹ ÔMæârôèQxàž|òIV¯^Í¿øEþìÏþŒ|!Bá¹.--$ïK°ç•ýœØÑ[<”RLLMëOh.9F=T,J‘­ÊR‘ÍR]]CuU5Ùl©dŠD"‰e]°ôo¨F£¹¸jÈ\­Qæ¦RËFñyÑ¡üYÖÇVðKªê*?Ú†2µUá΋¤üy°º$€rš›×Y޼ÜÅŠp¯B‘‹[9¤%–â6BFTTx'ø@ùo±2Þ]q›àM—RÆû* „,µŒDÌlndZˆh4ÎÈÈ?úÑøáHEE_üâùƒ?ølÛÆ÷}|ßÇ2-’‰¾çá‡7RÖ|`-«é;ÚÏhïSÃyÜI_…¿[ÑJš®Mï¬KÃ,?s˜i•5HÔ$He“$“I2™ TUUQYYI6“%•N“H$Êܯ4æZàŠ 3-QŒÅLk‡R*L¡[jш\¢Ba¨ˆ‚C¬+Ñ:åt«Â uÔŠ—+kÉéI4òÙÌ ³ ‘ò‰zo*¬Hli EF©à„D <( R ðRÄRňð%B‚D‚W$R |ßEÈt1Rï¬"Ó>W}A;:忥çJ:?Çáé§Ÿæþûïgpp_ú¥_âÁ¤¦¦&n£Tô½Œ2-%ðãµ @¡eÙd+3LNN’ËåÈò8ÏsñýR‹îe:Ðk”Ø-FX`ж-‰ è/&Å"‘AÓ 2•Ù¶M2™$•L‘J§H&S¡ëU"NÁ«Å‡F£¹Ö¸"È骎—e›¢Qt§ &ÅåËCâÀóà ¬OØ6¾ïE‡Š¥ð=¿\Ôø¡(Q~Б(Qñ>Š–’èn¢ƒ!¦€  _x衇xøá‡ihhàK_úôG«;—cˆ¾c‘‘áwß0d0ÑM88®ƒçº8®‹òýØúÝ<Ñ\dJ\f#+ˆaH ÃIJLLËŠEG$<¦§Ý}?þ6j4šk“+F€Ìæn5[ŒÇŒ ðÈí)¶Pxø” Œé3—)ßÇ÷JÖ)/´†˜#!»09…-{°d/–ìÅ6xvÌçÐ6ºìHá …Œcd80³_%püjò^¯Çk à7‘ó1åu"¥ ˆK‡!ÈxYdù.‚Ѳq2c™_X«HSí£x$EwºÓ»e½Ÿ.´3Ý‹â×/³ÂE1FÑQÌèCsîÌ<×ÊÅGpŽ%ç²(›žIˆxž‡a§Ýw>ŸçñÇçÁdbb‚_ù•_á‘G¡²²ò‚)—”Qõl+¬âcš& ߯ ÷b׫ð÷JÝD5DøÕŒ3ŠÀÅ5: #rÉ2‘† ¬#%çYÄûé7Q£Ñ\û\d¦ÛI‰»Õ´¹¾_Y9übìEp1õËÄFàZ Âl0*¼Ø“ßó놯PjŒ„ØGZ¾GÒÜKÚÚ)Æçv Ó‰À@Dð(„§YY ”xÁ£ K—•.?­„ ÛÂ6†€}e«|•`¼ÐÉ„³œIwS…xÔ`H,†!‘ˆðâZC¤Äˆ/˜¡ð0 ¤ô‘–‡îÊ—¤( )‹1&a˜|üî½_­!ÓÅG4Œÿ|ÏõJÎg_ Ï3¢£k}ï IDAT$;\tŽ%w«ãŠÔ%0S8ÿå_þ%ëׯ箻î*ë_)Å–-[øÁ~ÀîÝ»¹çž{øæ7¿I{{ûE>ŽâX a „ ­•>†a­¿%É7âÔâúäºèL·bÄ×e‚¤xþ‰Ûj4͵Âe §s¹*Z=ŠVŽ(`Û‹îG–ŒxâЉè®^(6|ÏÇ‹¬ž‡çûx¾‡éwcË=$å{¤ûHÈcáŸ~¬%Ï¥L"R¦á£4RF!S¡°˜/ÂI«_À÷'ñ½I|o*|^|Tʱ¥y*ïQ™x/^–s˜(,cÜ]Δ³œœß‰”V0ñ2"å@|H!0L£DŒxñ];ß±X‘R!ñQ…H ¾@¨ h=ž°M"¡Ït1èþZ¶†”ŠêàÏÃu=\ÏÅu&&&éë¯ñññx¢H”йĊ¤¹„C)Éf2ÔT×RS[W¡Ž\d,ÛÂ4ÌØUŠ“I¥®ëò»¿û»|ûÛßf×®]qï{öìáþûïçùçŸç¶ÛnãßøëׯŸß#åߣÒqF"$Z?eï‰æB(ýŽ–‹òºK%Ë£Ö×èoŸF£Ñˆ#GF@sËAñºKC™ðýÚg³zD>ÉÊ/Þ!ž!<üPXx¡µÃóqC±áûA!.Ï÷q=„x— ãe*¬×0ÅÈéÇ $¦Y…aÖ`˜•H™B¤L‚§ÝöÞ…b­ÈíÂÀ‰ ½ð(”rCq2‰ïOâ9øî¾79£uéÞ<•`$·Ž¡ü­ŒÖ#ŒT(D$†i„Â#x”áëH ׇVÃbLD`A ,'‘KK¹_}tè³]tçÿB,¡x”d°,´f,Îù‹ÌéâÃó<ÇÁqöØÇ;ï¾ÍÈÈHI{ÐbãÒ#„ ²¢’eËWÐÜÔB¶I$’AujÛÆ¶Ê³ qï½÷ò³Ÿý €ï}ï{ ðè£ÒÞÞΗ¾ô%>úÑžÑ-k¾8s|]é:è`Þ(‰á‚éV‘âû®…‡F£¹Q º»Ç€Ë$@Înõ(±Äà ÒUá‡îSeÂÃsÃÇÐuÅõð<×w±Õ.²ÆËTZ¯bÊ¡ÙÇH#‹iUc˜5˜V :‚‰æ\©$óÅ×ÁëÀj\|ü²õÅöÄí í„ÿJ.J‘™3pç’f‰K×™.^ åpÝaÇ1oèK/@|ßÇu]§@OOO=ó¹\.øXQTVVÒ¶ ÚºZ**²—å®ùU‰šíåÜÛ|ßgtt”‘áz{ûèëí‹×%IÖ®½‘ÚÚ2é,™Lšd*Å«¯¼Ê¾ðÆÇËcƉï½÷ç<—-L. Zhh4š÷#—M€ÌE|D¯ý°^‡çEâÃ/ÈðÜ`ç{Á£çMP!§Òx ÛèeêÃJ´`'Z2q†±Bä v–£*y<Aq~íËH¸_Åú(} ÒH e !“F !íYÇu†pò'q 'g¸lb$ÃÀäíôLþ"J6Ä"#$f @"!¾–†À4Ìà1Ìô¤ú-_Ê0]ð™\²æçâ}iHôˆ,uŽã099Éc?Âðð0JïÁuk×оðâ%_LÎ{¢Ÿ«ç»ã Ø>ÒÿgÙÞ4Ì’VŠ“'N±sÇN'°f³YV¯ZKUU%•|÷;ßå?ÿçÿ|Ú÷dåÊ•lÞ¼™ œß¸5F£¹¸,äLâ£Ü%¥xw8÷Â;Å¥Ù|× b;\×§BàhŸA-TPo¤‘Fº$€>œ,»£8ùò'ñ½â^xÊ¢üCœšüE”¨ˆeÆBÄ²Ì .$#¦inYRi'CÁeÛ*Ö(9s\ÈÅ!ó/@Jýí=ÏÃu]òù[þ*;vî@)0 ɇD21c›ËÅôôÀeþÖÍë_øžD¯™ù[2ç¿ÒºB*ÎÎd3TWW± }RJòù[_ÝÊÄÄM-tt,¢¦¦†ÜÔÉdŠd2‰e™DVÆRKWuu5—í3Ôh4ærS*@.[¬2ñAQ|ÄÂŒVnËMØ\7Ìä¸xÞ$Yñ8Õ‰Gf”V-vrV¢iVתRË˕ɴhÅ91]œÌa åá¹cxî(‘Œ1ÌŠ øÞªBìÂ0+IfVâ¹ã8ùãäs‡Q~)+6Óù)=¢wòñ¼F\ÓÃ4=<ÏÅ ³Ÿ¯Â7}L3˜¼K)1 ðÃt½Aˆ_"bX*:®æìXѤÔqzz{عkg´†›6mˆÅGäŽxEs>§éE¡(°Ï.>8—¯®ë2<4ÌÐà§NžbÕêUTTVpúëyí•­(¥èí;EEE¦iR[SCEe%ÙL†d2U”~µž£F£ÑÌ'—D€L¿{;C|øEñ¸\ya Ý ¾Ãuƒ rÇqâL’áÇTÛÏvÉô2L«~Öq\|ÑqÙf`sàg^á6ž3‚ç ÔÀ0³V5¦U23+0Ì$ÒK)䎟ìÂ÷séÐ\±™¦ì Ÿ¥ò£ä󡻜í+,ßÇ÷Í¢«¯0LPÊ@Bøø¾ Ó!EȵBx«k8o¯ ñqÙ™ëw¯T¨”ZPξùää{÷îãúÖR]]MçâNvD)E__/ét†d"m'HØ LËëèDiï¥óW£Ñh4š‹Á¼ Ó‰âó™âÃõ‚çÅ#,w ®ãb«i¶þC —H¢™dz9†Y}Úý½¿9W!lã¹£¸ÎyŽ`˜L«Ó®G“Dj1vrNþ¹Éø~ì¢ÑÞBCú5N}‚cwá¹Q]ËóP¶¸»XQf0LÚ_L¤áƒ/ƒ‡Q²°VÈÕm)wûñ\—¾þþxíÊU+‚Vú„=GÎßZXjA™ Û¶Q¾âP×aV¬ZÎâ%˜ššdbbœt:E*Æq $¼Ê ~ÓtÎF£Ñhfç’º`Íp>C|øEñá„®VŽã⦨”ÿ“jëñ’V¢Dz†YqÚ}i¦312}R§bW­Bî–Ý„•lF ;ÙìÀ) ’Ï÷à{9„ti©ú i»‹CŸ'—Ë®t–ß}VJáGw¢Ãªëàð•éi{}ÖG¹:EH *‚ñ+¦Þu]†††bÁ‘J¥JÚ^E”–0¸Ê†^dfh»”Û²Q(FFFq ¶m“J¥˜ššÂñ¦¦¦ÈåòòùàwÊs±|+šWù©Ñh4Í¥f^Hq"ÍNŠËKݰ‚4»¡øp}7¨í¸nÁÅwOÒhü’ÆÞ¸ì$U±¡LxD±WÛüí¸YßùXE@)—B®›BþV¢)Ì(fcÙuXv-…|?¹©ã€OUj«›¾É/1‘ë(&@¡|³ì³RÊ(8W¤$¨mÙŸ.äj¢˜å-Hõê82™t÷¢™ ó¡zÊ;‰DY÷ããÔØ5TWW155…ò¹|ŽB!ã8aF>¯\@^§©F£Ñh4óÊ%²€ˆò¸0ö#:÷ü æÃõÃ8‡‚ãá:¦ûMæ_`ˆ±¸7;ÕI*³&®FgĹ¢¹’oŸ¯ñ(äûq]˪IJ«ÒÄN4`˜Y¦&á{9lkˆUMË‘Ó;~'JÙ¡’;ÂDó´ ãU´Ç‚HLÚË&éá[zµÜi.Ïöæ“Ïk­dÂ"uÓÏãB¡@>Ÿ‚ ±m³¸9ŽÔ lË&‘L066ƶ7ÞDJÉïºsÖq  óöÛÛ±m›Ûï¸mÖ6}½}ìܹ‹t:ͦ›7ây/½¸¥`ÓÍ7‘L&Ïÿ¸Ì(¥˜œœD)°m«ìœ* xžK2™Ä0Œ²ÏcrbŠšššø³p  ÇuÂb¨~œHCJ‰D^5ç§F£Ñh4—Šy 3c?ŠËƒ ´*Ëvåya€¹ç…–õ]ª¬‡áäX‹Tå:,»9î×÷¯ÔIý•ÈÙDй !ÓY œÂ(Na;Qƒ•¨Á0Rd²+ÉMÃ) „Ç¢ú'ÉÚ{8<ôk(¿28'"·ªðS‚¢ ñ‹)x•Š‹w½g«$|5‹uö@óöÏn~€åË—ñµ¯5^÷èñmÛ›ÜrËÍ|î—?ËÈÈ(O=ù ÆHÿO=ù ™Lú´äÔ©žzòêëëØtóF\×åé§6ƒ‚µ×_7S€\R7¬ Û‰‚Ÿ½ð"ÝÝÇimk哟ú8£££<úðcøžÏ‡?z7+V,/ÛN…‰JÅDÜ Òƒû~\¿¨xÞêß'F£Ñh¦sI|>”R JE‰˜ÿe»r·àR©þŠjãŸbñaZudk?‹ÀåZ½¸_î‰õÙ…ˆ•YMùX…üS“ÇQÊEI*ÝA*Ýש¯èbYíŸãÆ)ä äóÇÁuœb¼™TŠ0CZQ°ÎœÐ]þ:çJ$>”¯Îi½ÿúû‚ õ‰ñ ¶ogF›ººZ¾òë¿Ê—¿ò«k¸çÇå>…ÏÂ?t'¶mq¼û8{ö¼‡RŠ—^|ÏóX¼d1+W®˜S?žòðý >Q±jIF£Ñh43˜_¬ØE&x[?ÂG”§ü¸¸ ëz8ŽGÊ ãɸ+¹€tÅ:Ê |ÍëÈ5Àì³caT#°f Û ðÜI&Ç’L5a˜,»ÃL11¶…Ke¦—N÷¿qpø÷ÀÊ!…D”"4 ç…6!Â"™‘Ë]‰ä*rm)›Æi¨çFE6ËØø8¯¼ò¿øÙO³uëÏq]7^1::ʳ›ŸGJÉê5«€Àåê‡=L÷±nÚ¶sÃ×Ïè¿¿¯ŸýðNž£Ít\×eËK/³ýíw¦¦¦š›oÙÄúõë@Àz˜¾þ~>þ‰Ñѱ—^Üž={¹ã·±rå ¶¿ýÛÞx“¥Ë–ð;ïàå-¯°sÇ.ÆÇ'ªª«Ù¸q«V¯ ë@¯¼¼•ÚºÚÛÛyýõ7hll䓟ú8ýý¼öÊkœüèѲm\×åÛ÷]<„!%¹\ŽÿóãœÇý1Ïm~¥|6nÜ@.—çÇý„7ß| €L6CÏ©ž8míî]{èééaÿ¾ìÝ»žžÞX$  ³°c!7ß²‘å+–Óß×Ï“OT=-öáÇÁñB<× Ü°¼@ úÊ/™Ô•[ÁâÉüUÆôã™ ·ßq¹|žïÿ¯ûåÖÛoAŠ3…>©S=ü‹ÿãçëßø·Ý~kY›}{÷384„‚õ¯ÿ%ßø¯³nýº3ö;11Á[onàcŸø(7ÝÄía¿¯¼üK:8zô““ôõõQUYÉ‘ÃGPJqìhwЮs÷|æøÈGïfaÇB.l§¾¾¥G-Û·ë¸Üó™_à׿úk|â“ãÝwvà8Í-Ílºy#7m¤®®–‰‰ ì?pÆãˆ¬D†aN§Y¼xñÛOGÅ¿Uák-<4F£9+ó„GV°ÖC\ÿC©8cŒÆTò?0ÄhÜGºú„ ²þhñ1óñ+D Œ Pçozq CFÓª"‘lÆó&)8Ý Kêþ;OýIàv% )0\Ïx¾ÄðeÿáùA\º/á¤[È`L¢D”\M.Ysÿü®¿a-?ùñ8Ð…eYlºy#?öÄ·‚4¿ ,ê\Ä–-¯Äm†‡ƒ¢žuuµd+‚ O‹Æî³1Ð??àÿFp,CƒƒxžÇ¢E‹Brüø ºB+È­·ßÂÓOnæ½={ÉçóÔÔTSUU…RŠÍÏ<ÇÛomÇ÷}ì„çº@ vJIgÒ,YR Ãá1ž:yŠ|ï²¶CCåKKñ<Ÿo}Ã0B099ɶ7¶±iÓÆb£³|ÅJ3›iñ¡Ñh4Íܸè$º‹0ÐÇõ>â @Qº‡çzH÷YãÙ¸Ÿdf¦YSÖç…02ê”Í# )ÈfÌKäÒt%§à= ÂDÈ „°‰*FÏ~ s;¶ÜÔIÒ24W,7ß› ›Rú()ñý 6ˆR*çf@¸ê1 ƒ›o½™çž}žõÖÅ… ÏD6L;11ÉÄø™l†ÞÞÞ²6™L€¡Á! …¶mÓÛÓ;£¯RJc+¾ô«_ ªºª8SÐ&“ ZÛZ8Þ}œW_ÝJUu5×]·†gžÚÌË¡ê\XIÆÆÆØþö;H)ùÚ׃êêjþñ‡8r$²~?èl&[–aª"Mmm­Üqçá `,v"qÚcxû­íŒŽŽRU]Å-·ÞÂ3O=Ã[o½Í¢E±X‹ygF£ÑÌ7ó_$ žø¾*Élä.XžG†Íñ&B&H¤—Û•±_ðïdrÊ+[&%4Ö'øè]üÂG›.|'W4sAÂDÈ,B$9ßú ³¢S“ݤ³AF¬Tz%cÁö¦Ê-ôŸ¼ ×òp ÏòQžç{ø¾X@”BÆw™eà{*«ÎèqÜùÁ;Xºt1MMs;_;-¤¢¢‚±±1¾û`ñâN^}[Y›¥Ë–’J&™ÊåøÎÿü{,hãõŸ¿qÆ~«««Y¼¤“ƒ]ÙòÒËl¸i=ÝǺç¾Ïß‹RŠÎÅï>NOO×_¿–T*ECcC,p-ꊩm=Ïå`×!&&&8zôØœŽqÅʼ³ý]Nœ8Éþ}ûill`llœýû°qã²ÙÌŒmúûØñîN„|ø#¦¥¥™î5«Ùµk7Ï=÷¿rß/# ]R£Ñh4šùàÒ]aUñŽ¥‚0­ªÂó=”ç’‘?›ÚÉJ}ýç ߇S½y¾ÿÐ1žx¶gÞösn\®Ù´UH£>p»:-¥ŸÇ¹Õ÷ ¹>ÌDsÌ^•ê”'ð\·˜’ÙóBáYÍü8Žhº»ËûÉõ%™L²xÉb2³Lªgöm¾ø¥ûH§Ó;ÖͶmoƱ™Lšû¾ð+$ :Ìöíïrë´6³ñ+Ÿ¿—ëÖ®¡ûX7>ü>üÏìØ±‹ÆëÁâÅ‹âç ¶ÐÑQ‹¥æÖÛnƶ<÷ì ;ÖͲeKçtŒÍÍM|òSŸ ›Íðæ¶·xêÉgxyË+x®J1¤ÁK?Û‚RŠ×ÝHkk ÄØTVV2<4ÌÖ×~>sGï#¡«Ñh4Í|"ŽQÍ-aªÓ £t2è—¸^绕ë{87Î~DaMò÷âí*ê>†”É‹šKÿ«¿÷6“S–%øî_­ctÜå'Ïœâ©çƒ;±kVTðÇÿº<÷¿ç)š ûĦ)XÔž¦£==£ïÁá»÷Ž22ê°µ5íIj«MPбq—cǧP@K“M*)سoœ¡a‡KR´¶nNám}@Qp|šäø© ÚZlw„îPa0ÿÀ Ko¿J±°ÝB)Å®½SLNz¬\š ¹Ñ¬á{89å±ó½õ‘šx»wwMðíûOÑ?è”õ·ri’ßþJ uÁ¾_~}œþxÜû™jžza„±qPX¦à~»Žµ«øÝûòüå _P ‘H“Le0 ÁïÝ 3¸9ÍÞ.‡‡~Œeá÷¿¾€éÆóË—d¸óÖ:<ϧûäo½32뾟~¡Ÿ–&›[nªâõ·Fñ}Åý÷°feŠŽ zûþëß'Ÿ÷‘nÞPÁÀ Ë¾®)Þ;0Å·¾Ýßü›6ä´ øC?¢¹Ñ¢½Íf÷Þ)WñÃÇGX»º€Gž‘bñ¢ ¹³Óô yìÚ›/÷˜Ïw'—WTWJn»ÉflÜçµ7ólßé`™ð¥Ï·‰ñ½ 03H#OHV 6ÂØ _) ŽÃ‹/måÇmæ…Ÿ¾Šë¸<öØw»Ղ³d ÕhRb[%VË= F£Ñh4— ="ôÒQ%î8‘ÂŒÅÍæRäîBpÅoþþÛñëTRò•Ï/â®;‰n«ÿô•×&‘€¯þj'×UŠ©¼øj?¯½1Äçÿ·< u62LÛÚœdíê ëlН~qAñ KX´0Åø£%¡xaË ßþÁ ”R<û³!¾ö«Í<ý ù¼ÀW>ßÈGøæßà·Ç9x$Ïî}S\·²Ò0È{•$ Ø´ÞãàÑQötø³o bZ‚ÖfÉúëÜysiÀ‰SnÜß÷~8I4t½âçØÛçS•58߸ÊAÈàóöC!+Tãû>ßþöý<úèS¨Yê¾H)ùô=¿øÓ QÜ»˜Q:ñœÇU¾(_ïkúúsÛO©‹’ïû477qç]wœçX¯ræIˆ!°L Ã4.j ™F£Ñh4š‹ÇüZ@f›GP%Eí”R8SŠú~žhr+Ä…Þ¸” l655Mä ¯‰ºd IDAT Óüõ\Ë©Þ<ÿýÑÛ_`óO{ioI²áÆ@t”ÞUokNÅVŽRLi „Éç>Ýɲ%µ¼õîïí'—÷è€ɳtqЦ ÃôH&3x¾ƒ”æiÞ˜¹#„aaš)Ò™J i²`A ÂLŠtÚESeÛܼ.MU¥dûŽ»÷åW=æqôØ5U’ÖXeÇÝÒ$ËݼTTߥ”s;åO TÄ•ïMÆë& u!øÍßü¿ñÕϳõµml~æE¶m{7žD:ŽËÃÿªª+1M)Eä[ú_Àïå B¿\ìxw'¹\Ž›nº<ˆ¿ sûN8ŽÃægžãŸüXÙr)dœÀıF£Ñ\¹Ì¯™m (˶%„ÀñÛËxî(†YÅùMÔXtQ<^½<Áêå6¨ ú]þëß 1>®øù[9nXcÑÖl²s¯À7'Ù°Ö"Îl¥=}M ÓïþÏíóQþ8ÊFéð³×)VÓžÈ5ÇÂ!“Iqϧ?ʽ÷ÞÃÀàO=ñ>ú4]]GxöÙ—ùå{?UÖ·vÃ:?|ßç©'Ÿæ×ã×.÷PJ8ówÞ²,<Ïå½={Y¹j†a`RÈ0ÆCio+F£Ñh®pæU€”V+Ž&±ãŒ È pYÀ”·ˆ”q€Üd™Êõçf É2Ñq6u¤X»ª‚{ÆuxíÍ!î¼¥–Û7ÕñÂKýŒOº<úÄIwO²°5ÍÐpžý'9Ò=É7ÿÃuüÉŸïaù’,­Í Ò)ƒ®Cñ˜öLQÐ}"Çÿó_² 5ÁöãAfañ¡4Hð‘¶ðúv‡BÁçÑ''8| ††]qÒ`a›Í²Åçþ·ß$•t´™Td%½}Á@m;øLî¸9É–×óLNù<úä$‡ŽX´µ xìíréé÷øÿ6{¦ÝÌŠòÆQ^ èÉ@l*¿€“’ø¾IïèZ¬D`Ý¡õK­-MüÎïþð¯¾ÆŽwßcÇνg´pèt§sgûÛïPWWGcScÙòB¡@_?UUU3ê(¥èíé%•N‘ÉdÈårØvËã8x•RÒ×ÛG:“&•J¡”bxh˜|>O}}}¹›‚©©ƒƒCTVTÄi|K÷…ÔÖS¯ß°žçŸ}n¼!X6O©‹5F£ÑÌó„.„(úìÇP†„ë„‚Qç.RÆ?àN Ôõ¸Ùø³ÄHÉPxœßÄó“nfÇž þ™ç{¸í¦Z*²¿õkÜÿ£côöØòÚP¼[ßÖ\S9Ÿ×¶ –ôŒuQ{ŠÞ>3¥é·V³cÏ8o½;½|öSõ,h ÒåÖך|íKÍ<ðpão¾3o»¸#É—ï­c°³â¹ðή<ïìÊ—©ºÚÉ= &™•’_¿/ËC?ž ÐcËë%ñ8J±°mî.GÁ6 üQ”æ|¶ë±S)äŽA°ý©¡5x*MB¤Ü©âsFŠÀå Áú kٸ醸s-6æÎluR¶¿ý×­½.ˆ ÏÛ7~¾G}ŒÚÚÙ°a=ŸýÜg˜Ÿà»ßùI%“¬Z³ŠW^~•õ‡¿OuM5O<þžçqôè1LÓäŽÜÆÂŽ…<ô?¤¿€t*…ïûÜ{ß/ÑÐØ€ïûÜÿý§ª²’ÁA>úѳdéÞÛ³—Ÿ¾ð3ªª*ñü å¯}åW‘R²téì'ôôôÒÜ<·ŠðF£Ñh®æE€”[>BT$F+ˆÁdSH‰”‚чhPÿˆ9P>£ÛÈVÝršþÈ B$æ<¦ßùj'ž§f¤¯]´0Å¿ùÝ¥8npícš’e‹3üñ®äà‘ úú ä Um­IšŠûýÿh5GŽM00˜g*ç‘LHš,–t¦)©ÖÓÒ”äžÕ³ÿÐããK:’45F®N«—§ùã?\È¡£9z Ø&4Ô[t¶ÛQ¼Û»ñÆLPœPA[Kñ£üä‡+ùÀ-@‘°‚ÛÌ¿ûÕºOèéw™œð±,¨©–¬Xb!e±hàŠ%ÿ×ïUqèH!Ÿ‚ãS™´5KêkÏÁ%N¹øÞ¨@ÄH#Eºb ž;FnboÐNÀáþ;7)á£ Ï 3"c,hÑY°NC$ú…ñwn6zzz¸ûÃÂ0‚/ÅÉ'y䑿÷þå¿`Á‚6¦&§øæ_þ5ï¾³ƒuëoä…çJ*•âßþÑÿ‰i<ñø“3L”»wíæ·¾ñ5êQJñýïÝOCc_ù/#¥äå-¯òãÇ~ÂoþÖWÙ·w?¾ïóßù:ÊbdÇAÁk¯nåÓŸþ:—t"äóù²Â‚5µ5 h¢Ñh4ÍUȼ¨êp4QŽ­ „.6ÒGŠ’I¦” j85ùZ3ÜB/ùÉ.é%ñª8×QàO¡¤œsÚÞ%‹fV0èì˜}aÀ²Å–-Î̺ ™¬Xš‚ ”ÚëLÒ)ƒVWÄígk›°%+–¦X±4÷YœüB ¦Ú¤¦Úˆ×Eý47šÒQºc0 AG»MG{i1¶ðoÚîM–uZ,ë,¶Qªtÿ¥£˜Iï1Jdá UX´”ï21òJñ8GûÖ34¹ŒTJbš†!BáU9—ÅÉtäÀ'J\ù4@àö$Ã÷j.oMn*ÇÈð(õ Å t;v좹¹‰ÜTŽýû P4·6sèàa6Ü´ýû»øà]w’ÍdPJqó-›xíµŸcšVœ`ñ’Å477¡”"—ËÑuà ÷|úStëÆó}ªª*9uªÇq¨®©f``m¯¿ÉÊU+¨®®!™H¢€ÚÚZ¶½ù X´¨۶˾'5Õ5  Î<0F£Ñh4W<óA½UâŠExW;ø“R†¤’¡©_ Ê~‘Œu€©‰]V5¦U7‹Ä%,ésŠý¸tœGýÅ+…pÉQª€ò‡Qª@©XJfVb˜ˆ›{+(DÜ»Žý"¦e`&†!1 à ž‹¹e‰ññ~wÁŠEÇyøäY¶…aH …B¼l|l ÇqؾýxYEE---LMNQ]S¯«®®FüÿíÙ“çyÞŸoé>Ël˜ÁF$E ¢ˆ’e‘¶$ŠT¹mve±]V¹’»Tœ¥R•‹\ä*ÿƒs‘²]®TâøBe_ÄŽr#S’—P Q$ Q$VÄ6`ö™ÓÝß—‹oé¯ÏœY 0Àó«:uúôvºûôßÓïó¾/࢙RAJ…ƒÆŽãó«óBâò¥Ë¸zõš×ºŸûÜ/AJ…#‡ã·ç·ðÆÞÀßþÍßáàcñÕ¯ÿ:ÆÇ&ðßø:Þzó-¼öׯaaa/½ô"^øì§ãwE<¿»=ƒ!„rw⨽!΂…ø¤[( %%t¦qaþßâÉ]ÿ ZºÜ‡ÅÙalêeHÙ†”"vS¯w[vV´¼¹ÿÚcïߛ㽺‡»óNâÛÎ-‰žµ+[» [Ív¥±LÈ:£ÏBgnк²øs«WÜ6xãôo¡Â.´2 ­eJ¹è‡ŠV,iDDî´ìîNEÄër'A ¥öìÙƒékÓØï“Ð÷íÛ‡“'Oáÿ“ßl\ÓÐOcÏÞ=8{ú,>ñôS€³gέ=ºd»‰]ÐZã©§žÄñçÇýÄþÖâèÑ#8rä0z½þüÛ7ßx /¿ü2:^|éE¼øâçñ‹ ¿ÀŸþéŸáÄ'Ç„÷7nàùOž¸ý p‹µÖ0òNßø0Ö=Ý´¶Àâìë0Õrc_k°«€¹k–`­Æim™ý{s|õ+{ðÕ¯ìÅã÷£¹eÜõ·v¦š†­fb®G@ç»1:ñ™(>z+aeñçqùÏ|3 ŸB–eÐJ!Ë42­¡µ‚Nî ¥’¼!¢jÖ÷\?¿áAC寓Tw&>ÄåË—ãç>ûi”E‰?ÿö_àâÅ1?7“ïÂéO¾ôòñúë?Äký=¼ñÆ›ø›ü-6:)%^ú•Ïã»ß} ïþä=,..âÊå+øáë?|øáiœ;w«««XXXÀòÊ2öîÝ k-~øú0;;‡ÕÕnÞ¼‰ññ±(>ʲÂÍ›³Ø·oߺ߽<$·!„2t¶=¬Rƒò@|ˆ°ÞZ# •‚TZT¥‚Ö +ås8{ý_âØî?`P•s˜¿ñ=tÇþ²Ö£~?¢!`—»ì­Y­[JT¿·Ü ïÕvïS0°fÖ, «U½N»ûòöA€µ–çßõU¯§~ñ%\˜ù"ZmV+CžçÐYid™¢JKwo$Q)“Äê‡hxô·›/|áWð?þûÿÄ—_yBt:ü»ÿoð¿ÿê;ø“?þo€GÀ«¯¾8ñüqXX¼ýæÛé╯|ø_ÿÝn°gït:Æw¼òê—Ñívñ÷÷ñ¿ú?˜˜Ø…O<û4ùþkßÇÌÌuìšÜ…'Ž='ŽÃ¸zõ*Þ~ûm½‡?ޝ}ýkñoþÔÉS8zô&vMl[ùÝþRv°+’B¹/¹k¬Ú§ï>…$æ÷añ6 ­$Œ’ÐZ#³Ue1·ò"N]ÅS»ÿ 2=ë"!so oFgô„P12pb À°Xô [[NX>÷÷Ç5w[Ìl°Y ¸æJ¢Õ}J»ûªœÅâì›1磲Þüð›¸xý—ÑêdȲ ™Îe¹eZCi­$´RÊ W{XDñÙ´Ùõ#c÷)õñ®ÿÛK© îÐjµ‡?„©ÝSøÙOßdzÏ=˜Ú=…oýÞï6Ö«­S'NljãÏ9ñð½`j÷n´ÛmX |á‹¿׫ÏAâÅ—>Ï¿ôË~?õ¾žzêI<ùä±øÙ†bøú7¾æ’Îm,€7ß| ¿ökÍNèÛÅ­þ%6£»;ë$„BîCË Â#TTRBÁ•Þ4FAkc,ŒÕ0™19VV?‰w.ýg<¹ç°«ãì;½•ó¨ŠtÇ? ¥Ç’hÈ:BÖÙƒì*,¤oXØÚRçð‡‹æÐËÆë¶â"¶B³WÒãÈÛAg“qÕ¥3X^ü)àípóK»ñ÷?û–ŠChµ[håÚ­ÜG@²:ò¡”¯†å¬FJº.×R¬Íõæí çdÒ s5Ðoüæ7±0?¿¥ugffðÇø'x챃¸ôñ%\¿~¿û­ß¹;¶Ž(ŠŸûÜ/áÀ£î8ú±•¨GºNˆBmö½;ñž$„B†É]ï¦ëyÖå€X k­{Ê«c ´ö‚¤R06ƒµ«v~vå?âѱoãñ©ï¢ª0ãûèŒ<‹¼sÔÛr6"`j‹ 4 2z ’»-±…K(·+€é!”Ñ]ïûT¶ yû ”G¦ZÂòü»(zWãz®žÀ§ÿ)¤E«•¹Wž£•gÈ[²\#Ï3ÉT´`))]Å4)}•¥ºÖN%=ö<¯í q¹Rû÷íÛ‹}ûöniÝ={öàŸÿ‹ßõ«×ð™Ï¼€>â:ox?ÞáýÚ·y–exîø³ƒpÛõëÒñV³ð[€ª.¦ñÐF „Bn¡D@Dlç£ ÆÂ !œËZ 뛌™h·‰Í§ƒ‹óÿ ³+Oâ™Gþ™Z`°¼x2ö É;G¼-k+B*ÀV1yÚBD1‘mCYßûÛVå"C`{Þ^ÕP%ýA€õÎAgSÈÛBª‘¸¯ªœÇêÒ‡è­\ŒÛ«ðö‡¿Ž3W­VŽ,ÏœèÈ3´Û9²¡d€Ppýä}YæwS¬ĆϱjKÕÚDòþyR@g“Ðù$¤ •¼,ªb+K X½ÔXzî1¼}曘_9ŠvGG¡ÑÊrä-ùb$ϲ¤ VmŪUº²Í=öþ0wÒç´Îøø8ff¦ËËKñùQäÎØŠØØª™˜G–eèõzX^vùŒ¤Ï]“ª.~»tOB!Ãäî%¡ëUŸ‘=ˆ!ÜÞ.¡%bnsjé¾öªÎŽSôàçÓÿ箟Åá©ÿ…ýãïÂÂÚVßÇêÒiä#hwAÈVL‚¯»©oEŒxl  lŒP, ‰ JÜ´ôç:ìA‡u‘TþxÝ»õó¬-ýò´óùFÃ1¿O¸¤r¥' ³I™'Ë,ÊÞ V–>@Ù»ÖØÃôìcxïÂ+¸6÷²\£ÝÉ£½*D:ro½Êó ™ÖÈü|g¿Rµý*}¥eœwhîPGCꉉ‰(@~vê}¼ðÙOÇÈÂN=Çš;‰7 ØÍ®z»G£µÆ‘'ŽÎœ>çK!\­¡uæò–dݧÆYLå-D‰!„‡ƒ¡X°ú£ €„”6¤ïûÑ'>Òž2ÍHÚÎ#ie¸PfWkw]ž~ê¸6}KKË0Æàµï~Ï‡П uoq­Kn£dû/ aŸÜÚwGÁçí ?ºˆ“ïBY–€²,‘·Zèvºèv»h·;. ’¥¹KŒ^B!ëq×H:ð ŸûEˆÓ‰±ðQŠPÞÒWšQ.b¢ŠÒõˆP._ (*h­P%ŠâNOÁû—~»:?Å#oáÀä;Èô²_«WP¬^ ¡³ ¨lJOBg“ª›+æitd;¢%Û‰€™¯¦ë—¬§×ba­AU΢*n¢,o *®ÃTËkÖ¬ŒÆÇ3Oáµçqyö€¶³ZutÁðâCkè¼Y¦)'P´V~~ÝïCù¦ƒ"&×ï¡ÒU3ò±³ÄGJܺ~'­¼…N·‹gž9wßý1Š¢€µ'ß;… ç/à±Caj÷ÆÆÆ†ZžwÛñWÛþ}¯ó7èÜÉ¿=[ünc æææ0{sW._ÁôôL\VUBhŒŽŒbll ###èv»È[9²,ot­§ýŠBŒ8~ÖÀ#f ÄÝM’ö ƒwcŒ+Áë»»^ •­P•eU¡ª ªªBYT(ÊUY¡,J”•û\Š¢€©*E‰²¬PVʲ‚©z˜ìžÄ]oá‘É÷©•u-BæÐQì‚Ê&±õê6±r¥ùi¾Gj©2~ƒÍž ‹0 ÷Ÿx±¡€ð¾…ã3Õ"ÊâF-8Ê9„fý«péú1œ¿úI\¾yÆv¡´ J{á U³z•–Ñv¥•t$ ƒP©“Í]K_v×G>RñÐ×\“mÜ Ô•Îü;|¡!!’‚q·&Â߀1î^.ŠËËËXXXÀÜÜ,®ÍLãƒÞÇÜÜZ«€QBr7T^ØZ‹²,¡U†±±qLìÚ…ÝSS˜šœÂ®ÉIŒŽ¢Ûí¢Õj»ªXI2:!„BÜð÷£œ³f¨, 4¦‘0OJÀÚ¤“Ѫò9&vÖR¢”¥‹†”ÒnKd™FQ”Pºp¥4(Ëe¥1_|7/ §.®brä$™ø ¦FÏb´=tðoM/‰8¤…Òãªër%dÇOwû" !Z’ö{¸+—qÂw¦Zò¯e㦫ò&¬)6ÜÃRoÓ7Ç¥›OããëÏ£2#NtdÎDEˆZÄH†®£Y´U…¦‚!ÚQ¯/¥„ÒÎR—V»JÅGš£ÓÏNØ5sŸÜ}›åÚí6ʪÄdUáé§žÁ¥/âʵËÑòp–E2l*c` Ð팠ÛÁøø8&&&0>>±±1tÚmäyËWÂR„BÈ& M€ô[±Â¼àçBÄœ—˜®à" 0€Ì‚-§B)OF—(µ*KèJ¡T%´’¨ŒBY¸èGYj=qQ‘Êd˜ë½€—ÿ!*c 0]í3˜=‹©Ñs˜ý”ê5¦Z€©0!²¤ݰC…(…ðOÔ·†K¦‡­`ý+”Øu¯^-4̲ËëF4ú1VáÆü#˜ž{Ós‡q}á0VÊ)Há":—Èý@YùÆ€Z&D+H¬W*ÎÓJy{œ‹ˆ„yÒ7”Rù2» RRªXÀýöµíª¾Oúï›KjÃÊtÓ6. 膒㘛›ÃüÂÂö„BYËГÐ×!iNˆ›güÞ¸²˜ÆuàpÕ •”¨* UTJ¢¬Œ{Ò^”¨*ƒR;ÛVY”¨ŒAYjoå*K·Ne ª*Ãl± 7f>…®تÄh÷#LvÏbjävC·}c}Ÿ¹-\Å€ Q\‰†uÊY~„·bqQnYHl••b3³‡0=w3 ‡qcá,Z1çB)…N&¢àó‚M*D=¤f„*D7TÊ[µ¤Ïñ1×£ÎíðÓJú<Ÿp/¬ízÞ¸r;|P—ö‡\‰$CBºkœç9:ÆFG±Ò[EouEQ˜ UU9»"l0ßO‰ê;úAb‚½‹PIhízÔ´Z-t:t»]ŒŒŒbtdnÝN­ÜUÀ¢íŠBÙ÷Ä‚µ™q4áíY BFØX’×()]Ÿ©*¨ D*‰J+Te]Teé>‹ª,Qú<’ªÊ¼±¨*'FŒq‚ă^õ$.Ï?‹7]>J&ÐΦÑɧÑɯ£ÛšA7¿ÑöuŒ´oBŠmMk±^`”ÛúÜÚ@bie‹Ë“XXÄâÊ–V&±ØÛåÕI¬–“I¤A"o§Mñd-DdŸñÑ 7íÄ„VÒçph()¢‰Q¥â{h*¨¤tvº¤J³\ ÞÆ¥v´]mÅÒÈ[pù.ÒuÖÞ•‘Q¬öVÑëõ¼p®\ŽTbÇ¢øØÒÈ…Œ‘?…<Ë‘å9Ú­Zí6:íÚí¶+ÁÛjÅœ&W,ƒÑB!d3†.@€ÍìXB·qß ñ ¹•—",-”‘¨¤”•$W*¥œÀÐÖ¸dôJkS¡¬ LÞ *£QU¶ª`¬õÅ ð SU0¦…U3‰••c˜^´1i0¦B¦æÐm]w¯ü:ÚÙ”ìAÉ´ìA©ð^@©2ÙƒV”* Ðå¨L†²ÊPT9ª*Cir”UŽÒd¨*7]™ «Å–¼ÐXîMbiu„ô! Yð tòºIZZiJI…Qtx{”z(™ ×(ÐÙ¯¤{b¯½ÈP!©\@*×ËC©¤§GRÙª¿Ìn=p{ðÅG$̾ê›Ö¾yc«…nQ èõ|A…UåmXÆFÛõÇv`÷ësì ŒIDAT“{Е‚÷vø=´ÖÈs'FZ¹«z¥}…P&ÜmÿàÜ«„BÈÝàž`#¤y!ÖZHXé¢!Á¢e„€°€•4 cQUÊG3¼©$”uy 6Ú®Œ³²ÄiëÖõO–«ÊùñA|ÚlŒ…1¬Œõž}*xµ±böbiÑÂ.غŠNRåKøîÊÖW²¾¡%”ìA…Eóš„ øòÄu¢{œB™ è‘ ö뜺 wZòVD"€µðPJB ¥bd¤!8¼‘I5«TxÔÇÍ}Ô#i0XßuŸæ}ñàïÿ¾¤ûq¶ŸÜQ(]µ7SU±Zœ‹¢ÙaW9xà –6'„½ ×:TˆÕÞ”Ž’´Ñƒx¯B!ÛÍ= @ýŸõF–,!`’hˆŽ)`…ˆBÄ( ©\‰2¦’^8T•mجl°]ùȇ‹~¤b#}wÓÖÔÀô½1 í1¡ÔpZÖ³ùn´ãyËÚ†¾îõJ-á:…ÁO|¤êHCä*)`ÃJlWR¤‚¤OLÄ,‰HIDŒò¢$Z«ü±)bs!„DŒv4Ï¥àö`‹@hv‡äüSûöQ»ú40Ö—sö÷ “Ï·—ØôRÖ¹ A¼‡ßE„{^È5“„BÙœ{*@ëEC\Ô ÎýhØVŒ”Œó­€1°°BÂH ãóIšB¢ÎO¯}¢>±¾íb´#ˆ'Z¬uÓ®:‘…µ"V* óB’ð`b­&±¤58¢1`ÂêCäC"F „·’$‰ß¢ Ñ úEˆðÒ×$0Š›\á-`õ@:¡ÂUzìéùõÿþýÒûËôF»Ÿ¿­OXgÞÇÝ#ÂÕâ>Dï’ûZ<B™BÙnî l ñŸøç]D$X³¬tÖ)i-¬”0‚Aú(Eå°:‰nÔb#ˆ cë|S'JBT#¬ $bÄ4B4Ä»±‚©"9çA˜¤“rßÓráç…¨LH$^pÔÓÂç†xÛHjÿi$ˆ oŸ ŸUÓÒ¥Dm¯ ƒ4¿pŠÈ=–}â¨1`[Ðö° æÒ{? lSQ–ÕQ³Á+äÎiÜ{ Ûcäqm!„²)÷ lž "BHu²¶A +¡„…•Ö‰‘JúÁœjT&î‚ÅÅÔBÅ¢/ÂáÅG&ˆ‹`ñŸ‰¹¡gC<­Ð=·îõˆggø1~2-Ü4‚u.záç÷‹„¨HÈˈ 2’‰Hò6BÉ\ÿ›„ÜŽ`³€ÙÖË)<¶L¿韗Bñq÷X_`<ø¹I„BÈ0¸ï°Y4$DÜòè H颀‘Æ÷ñbÄë‹ò±¨Å‰Ó‰(ñ_`lˆ ä\e"DÁaŒ Qk!â-eÑ»¿E"üž‚Øpóü@UÔ¢#FG!Ò ~žôI¶"‰\ˆTp$U´œ¦넊dõö!é½~:ÜÌñ§±Ñ€ƒ¹šôZÔ÷6Ÿºß{ØãƒBÙîK ó©qïi®…Ò5k3 V¸yFÂø„1°Ò /2ætˆ5¢>a½ùŽ˜ì<ú¶Ot !8¢åJ`sâN°M;Vá³óŠÄ$ðfDDô½7çK¿ß´JUº lSG6‚m(üåx¬µšq@·1¼>„Byи¯°~4$]¶6ñk‹µPuùÛ‘~ž^p>š!-¬UÞB¥ê¤rÔ"% cë5"$$ ûÏn>ýÕ‹&¡'CuQ—Ê U”„O8‘ˆ(>b"ºôzG@EFL²Âé:H#©èHr£g¾ï÷‰i<´¯B!„<ìÜ÷$0È–Ò¿Ì ’ÊRÀZ1"B¾ˆSÖÚ ‚$T ½<¬uy ÖV[ÀŠ(hÖŠŽú{Â2 IÞ¤óúÇçdüÄ–çGRO׫ö /DˆÂD •·U%ËÝ´m~W*:bÏhr€°hœ…!„BÈÃÌŽ )[‰ŠlIŒ !¶oÚ­k¡D P‡<°V„ÄíPcÃjÕ´•ÅéþqyÔƒ,K¶!BâùúݱrbDT¨óA’ek*üÛWý']G¢):Dr\H¢kB!„Bv¤ lI—¯'FÂvë  ÀʸíúÍE2Ï ÑiD!VJŽeÐñ7í}¡‘~q’Š"it"9ç¸jÁh ‘ T—EcŸkŽÑB!„²;Z€¤Üž6$pÑìU°õúñ=yú¿n”c€øt̃ïI‚EŸ”èÝ\f×YкéqlvLÍs¡è „B!ñÀ”MňL£ ’°5¶/QžÒF‚õòZ¤¬wÍ_ï|ÏKÅXGL¬7È6µ5Á±ùñB!„2ˆR€¤ #ý9 }‚¤±®›;pݰ޺‚'¤Kôíc½}oƺQÈÆ®ŠŒ¸nº¬y„B!änóÀ ”þóz‚¤Ý5¢¤±›u’Ä!-^#Xn‡u·˜Þ¿Â­  B!„²] ÷ B!„ò°!ïõB!„B(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!Cƒ„B!„24(@!„B!CCÇ) Xˆ{x(„ÜK„B!„íÆZ§Åùó³vƒu !„B!dÛ ‹B!„24þ?,¾K–îÉTIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/docs/Makefile0000664000175000017500000000434000000000000017352 0ustar00tseavertseaver00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html web pickle htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf .build/* html: mkdir -p .build/html .build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html @echo @echo "Build finished. The HTML pages are in .build/html." pickle: mkdir -p .build/pickle .build/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web .build/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p .build/htmlhelp .build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in .build/htmlhelp." latex: mkdir -p .build/latex .build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex @echo @echo "Build finished; the LaTeX files are in .build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p .build/changes .build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes @echo @echo "The overview file is in .build/changes." linkcheck: mkdir -p .build/linkcheck .build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in .build/linkcheck/output.txt." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/api.rst0000664000175000017500000001245000000000000017216 0ustar00tseavertseaver00000000000000.. _api_narrative: Using the :mod:`repoze.who` Application Programming Interface (API) =================================================================== .. _without_middleware: Using :mod:`repoze.who` without Middleware ------------------------------------------ An application which does not use the :mod:`repoze.who` middleware needs to perform two separate tasks to use :mod:`repoze.who` machinery: - At application startup, it must create an :class:`repoze.who.api:APIFactory` instance, populating it with a request classifier, a challenge decider, and a set of plugins. It can do this process imperatively (see :ref:`imperative_configuration`), or using a declarative configuration file (see :ref:`declarative_configuration`). For the latter case, there is a convenience function, :func:`repoze.who.config.make_api_factory_with_config`: .. code-block:: python # myapp/run.py from repoze.who.config import make_api_factory_with_config who_api_factory = None def startup(global_conf): global who_api_factory who_api_factory = make_api_factory_with_config(global_conf, '/path/to/who.config') - When it needs to use the API, it must call the ``APIFactory``, passing the WSGI environment to it. The ``APIFactory`` returns an object implementing the :class:`repoze.who.interfaces:IRepozeWhoAPI` interface. .. code-block:: python # myapp/views.py from myapp.run import who_api_factory def my_view(context, request): who_api = who_api_factory(request.environ) - Calling the ``APIFactory`` multiple times within the same request is allowed, and should be very cheap (the API object is cached in the request environment). .. _middleware_api_hybrid: Mixed Use of :mod:`repoze.who` Middleware and API ------------------------------------------------- An application which uses the :mod:`repoze.who` middleware may still need to interact directly with the ``IRepozeWhoAPI`` object for some purposes. In such cases, it should call :func:`repoze.who.api:get_api`, passing the WSGI environment. .. code-block:: python from repoze.who.api import get_api def my_view(context, request): who_api = get_api(request.environ) Alternately, the application might configure the ``APIFactory`` at startup, as above, and then use it to find the API object, or create it if it was not already created for the current request (e.g. perhaps by the middleware): .. code-block:: python def my_view(context, request): who_api = context.who_api_factory(request.environ) .. _writing_custom_login_view: Writing a Custom Login View --------------------------- :class:`repoze.who.api.API` provides a helper method to assist developers who want to control the details of the login view. The following BFG example illustrates how this API might be used: .. code-block:: python :linenos: def login_view(context, request): message = '' who_api = get_api(request.environ) if 'form.login' in request.POST: creds = {} creds['login'] = request.POST['login'] creds['password'] = request.POST['password'] authenticated, headers = who_api.login(creds) if authenticated: return HTTPFound(location='/', headers=headers) message = 'Invalid login.' else: # Forcefully forget any existing credentials. _, headers = who_api.login({}) request.response_headerlist = headers if 'REMOTE_USER' in request.environ: del request.environ['REMOTE_USER'] return {'message': message} This application is written as a "hybrid": the :mod:`repoze.who` middleware injects the API object into the WSGI enviornment on each request. - In line 4, this application extracts the API object from the environ using :func:`repoze.who.api:get_api`. - Lines 6 - 8 fabricate a set of credentials, based on the values the user entered in the form. - In line 9, the application asks the API to authenticate those credentials, returning an identity and a set of respones headers. - Lines 10 and 11 handle the case of successful authentication: in this case, the application redirects to the site root, setting the headers returned by the API object, which will "remember" the user across requests. - Line 13 is reached on failed login. In this case, the headers returned in line 9 will be "forget" headers, clearing any existing cookies or other tokens. - Lines 14 - 16 perform a "fake" login, in order to get the "forget" headers. - Line 18 sets the "forget" headers to clear any authenticated user for subsequent requests. - Lines 19 - 20 clear any authenticated user for the current request. - Line 22 returns any message about a failed login to the rendering template. .. _interfaces: Interfaces ---------- .. automodule:: repoze.who.interfaces .. autointerface:: IAPIFactory :members: .. autointerface:: IAPI :members: .. autointerface:: IPlugin :members: .. autointerface:: IRequestClassifier :members: .. autointerface:: IChallengeDecider :members: .. autointerface:: IIdentifier :members: .. autointerface:: IAuthenticator :members: .. autointerface:: IChallenger :members: .. autointerface:: IMetadataProvider :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/changes.rst0000664000175000017500000000003400000000000020050 0ustar00tseavertseaver00000000000000.. include:: ../CHANGES.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/conf.py0000664000175000017500000001366600000000000017224 0ustar00tseavertseaver00000000000000# -*- coding: utf-8 -*- # # repoze.who documentation build configuration file, created by # sphinx-quickstart on Wed Jul 16 13:18:14 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. parent = os.path.dirname(os.path.dirname(__file__)) sys.path.append(os.path.abspath(parent)) wd = os.getcwd() os.chdir(parent) os.system('%s setup.py test -q' % sys.executable) os.chdir(wd) for item in os.listdir(parent): if item.endswith('.egg'): sys.path.append(os.path.join(parent, item)) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'repoze.sphinx.autointerface', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'repoze.who' copyright = '2008 - 2013, Agendaless Consulting' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '2.2' # The full version, including alpha/beta/rc tags. release = version # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. #exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'repoze.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. html_logo = '.static/logo_hi.gif' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['.static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'repozebfgdoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'repozebfg.tex', 'repoze.who Documentation', 'Agendaless Consulting', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = '.static/logo_hi.gif' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1464713044.0 repoze.who-3.0.0/docs/configuration.rst0000664000175000017500000003263200000000000021320 0ustar00tseavertseaver00000000000000.. _configuration_points: Configuring :mod:`repoze.who` ============================= Configuration Points -------------------- Classifiers +++++++++++ :mod:`repoze.who` "classifies" the request on middleware ingress. Request classification happens before identification and authentication. A request from a browser might be classified a different way than a request from an XML-RPC client. :mod:`repoze.who` uses request classifiers to decide which other components to consult during subsequent identification, authentication, and challenge steps. Plugins are free to advertise themselves as willing to participate in identification and authorization for a request based on this classification. The request classification system is pluggable. :mod:`repoze.who` provides a default classifier that you may use. You may extend the classification system by making :mod:`repoze.who` aware of a different request classifier implementation. Challenge Deciders ++++++++++++++++++ :mod:`repoze.who` uses a "challenge decider" to decide whether the response returned from a downstream application requires a challenge plugin to fire. When using the default challenge decider, only the status is used (if it starts with ``401``, a challenge is required). :mod:`repoze.who` also provides an alternate challenge decider, ``repoze.who.classifiers.passthrough_challenge_decider``, which avoids challenging ``401`` responses which have been "pre-challenged" by the application. You may supply a different challenge decider as necessary. Plugins +++++++ :mod:`repoze.who` has core functionality designed around the concept of plugins. Plugins are instances that are willing to perform one or more identification- and/or authentication-related duties. Each plugin can be configured arbitrarily. :mod:`repoze.who` consults the set of configured plugins when it intercepts a WSGI request, and gives some subset of them a chance to influence what :mod:`repoze.who` does for the current request. .. note:: As of :mod:`repoze.who` 1.0.7, the ``repoze.who.plugins`` package is a namespace package, intended to make it possible for people to ship eggs which are who plugins as, e.g. ``repoze.who.plugins.mycoolplugin``. .. _imperative_configuration: Configuring :mod:`repoze.who` via Python Code --------------------------------------------- .. module:: repoze.who.middleware .. class:: PluggableAuthenticationMiddleware(app, identifiers, challengers, authenticators, mdproviders, classifier, challenge_decider [, log_stream=None [, log_level=logging.INFO[, remote_user_key='REMOTE_USER']]]) The primary method of configuring the :mod:`repoze.who` middleware is to use straight Python code, meant to be consumed by frameworks which construct and compose middleware pipelines without using a configuration file. In the middleware constructor: *app* is the "next" application in the WSGI pipeline. *identifiers* is a sequence of ``IIdentifier`` plugins, *challengers* is a sequence of ``IChallenger`` plugins, *mdproviders* is a sequence of ``IMetadataProvider`` plugins. Any of these can be specified as the empty sequence. *classifier* is a request classifier callable, *challenge_decider* is a challenge decision callable. *log_stream* is a stream object (an object with a ``write`` method) *or* a ``logging.Logger`` object, *log_level* is a numeric value that maps to the ``logging`` module's notion of log levels, *remote_user_key* is the key in which the ``REMOTE_USER`` (userid) value should be placed in the WSGI environment for consumption by downstream applications. An example configuration which uses the default plugins follows:: from repoze.who.middleware import PluggableAuthenticationMiddleware from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger from repoze.who.plugins.basicauth import BasicAuthPlugin from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.redirector import RedirectorPlugin from repoze.who.plugins.htpasswd import HTPasswdPlugin io = StringIO() salt = 'aa' for name, password in [ ('admin', 'admin'), ('chris', 'chris') ]: io.write('%s:%s\n' % (name, password)) io.seek(0) def cleartext_check(password, hashed): return password == hashed htpasswd = HTPasswdPlugin(io, cleartext_check) basicauth = BasicAuthPlugin('repoze.who') auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt', digest_algo="sha512") redirector = RedirectorPlugin('/login.html') redirector.classifications = {IChallenger:['browser'],} # only for browser identifiers = [('auth_tkt', auth_tkt), ('basicauth', basicauth)] authenticators = [('auth_tkt', auth_tkt), ('htpasswd', htpasswd)] challengers = [('redirector', redirector), ('basicauth', basicauth)] mdproviders = [] from repoze.who.classifiers import default_request_classifier from repoze.who.classifiers import default_challenge_decider log_stream = None import os if os.environ.get('WHO_LOG'): log_stream = sys.stdout middleware = PluggableAuthenticationMiddleware( app, identifiers, authenticators, challengers, mdproviders, default_request_classifier, default_challenge_decider, log_stream = log_stream, log_level = logging.DEBUG ) The above example configures the repoze.who middleware with: - Two ``IIdentifier`` plugins (auth_tkt cookie, and a basic auth plugin). In this setup, when "identification" needs to be performed, the auth_tkt plugin will be checked first, then the basic auth plugin. The application is responsible for handling login via a form: this view would use the API (via :method:`remember`) to generate apprpriate response headers. - Two ``IAuthenticator`` plugins: the auth_tkt plugin and an htpasswd plugin. The auth_tkt plugin performs both ``IIdentifier`` and ``IAuthenticator`` functions. The htpasswd plugin is configured with two valid username / password combinations: chris/chris, and admin/admin. When an username and password is found via any identifier, it will be checked against this authenticator. - Two ``IChallenger`` plugins: the redirector plugin, then the basic auth plugin. The redirector auth will fire if the request is a ``browser`` request, otherwise the basic auth plugin will fire. The rest of the middleware configuration is for values like logging and the classifier and decider implementations. These use the "stock" implementations. .. note:: The ``app`` referred to in the example is the "downstream" WSGI application that who is wrapping. .. _declarative_configuration: Configuring :mod:`repoze.who` via Config File --------------------------------------------- :mod:`repoze.who` may be configured using a ConfigParser-style .INI file. The configuration file has five main types of sections: plugin sections, a general section, an identifiers section, an authenticators section, and a challengers section. Each "plugin" section defines a configuration for a particular plugin. The identifiers, authenticators, and challengers sections refer to these plugins to form a site configuration. The general section is general middleware configuration. To configure :mod:`repoze.who` in Python, using an .INI file, call the `make_middleware_with_config` entry point, passing the right-hand application, the global configuration dictionary, and the path to the config file. The global configuration dictionary is a dictonary passed by PasteDeploy. The only key 'make_middleware_with_config' needs is 'here' pointing to the config file directory. For debugging people might find it useful to enable logging by adding the log_file argument, e.g. log_file="repoze_who.log" :: from repoze.who.config import make_middleware_with_config global_conf = {"here": "."} # if this is not defined elsewhere who = make_middleware_with_config(app, global_conf, 'who.ini') :mod:`repoze.who`'s configuration file can be pointed to within a PasteDeploy configuration file :: [filter:who] use = egg:repoze.who#config config_file = %(here)s/who.ini log_file = stdout log_level = debug Below is an example of a configuration file (what ``config_file`` might point at above ) that might be used to configure the :mod:`repoze.who` middleware. A set of plugins are defined, and they are referred to by following non-plugin sections. In the below configuration, five plugins are defined. The form, and basicauth plugins are nominated to act as challenger plugins. The form, cookie, and basicauth plugins are nominated to act as identification plugins. The htpasswd and sqlusers plugins are nominated to act as authenticator plugins. :: [plugin:redirector] # identificaion and challenge use = repoze.who.plugins.redirector:make_plugin login_url = /login.html [plugin:auth_tkt] # identification and authentication use = repoze.who.plugins.auth_tkt:make_plugin secret = s33kr1t cookie_name = oatmeal secure = False include_ip = False digest_algo = sha512 [plugin:basicauth] # identification and challenge use = repoze.who.plugins.basicauth:make_plugin realm = 'sample' [plugin:htpasswd] # authentication use = repoze.who.plugins.htpasswd:make_plugin filename = %(here)s/passwd check_fn = repoze.who.plugins.htpasswd:crypt_check [plugin:sqlusers] # authentication use = repoze.who.plugins.sql:make_authenticator_plugin # Note the double %%: we have to escape it from the config parser in # order to preserve it as a template for the psycopg2, whose 'paramstyle' # is 'pyformat'. query = SELECT userid, password FROM users where login = %%(login)s conn_factory = repoze.who.plugins.sql:make_psycopg_conn_factory compare_fn = repoze.who.plugins.sql:default_password_compare [plugin:sqlproperties] name = properties use = repoze.who.plugins.sql:make_metadata_plugin # Note the double %%: we have to escape it from the config parser in # order to preserve it as a template for the psycopg2, whose 'paramstyle' # is 'pyformat'. query = SELECT firstname, lastname FROM users where userid = %%(__userid)s filter = my.package:filter_propmd conn_factory = repoze.who.plugins.sql:make_psycopg_conn_factory [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider remote_user_key = REMOTE_USER [identifiers] # plugin_name;classifier_name:.. or just plugin_name (good for any) plugins = auth_tkt basicauth [authenticators] # plugin_name;classifier_name.. or just plugin_name (good for any) plugins = auth_tkt htpasswd sqlusers [challengers] # plugin_name;classifier_name:.. or just plugin_name (good for any) plugins = redirector;browser basicauth [mdproviders] plugins = sqlproperties The basicauth section configures a plugin that does identification and challenge for basic auth credentials. The redirector section configures a plugin that does challenges. The auth_tkt section configures a plugin that does identification for cookie auth credentials, as well as authenticating them. The htpasswd plugin obtains its user info from a file. The sqlusers plugin obtains its user info from a Postgres database. The identifiers section provides an ordered list of plugins that are willing to provide identification capability. These will be consulted in the defined order. The tokens on each line of the ``plugins=`` key are in the form "plugin_name;requestclassifier_name:..." (or just "plugin_name" if the plugin can be consulted regardless of the classification of the request). The configuration above indicates that the system will look for credentials using the auth_tkt cookie identifier (unconditionally), then the basic auth plugin (unconditionally). The authenticators section provides an ordered list of plugins that provide authenticator capability. These will be consulted in the defined order, so the system will look for users in the file, then in the sql database when attempting to validate credentials. No classification prefixes are given to restrict which of the two plugins are used, so both plugins are consulted regardless of the classification of the request. Each authenticator is called with each set of identities found by the identifier plugins. The first identity that can be authenticated is used to set ``REMOTE_USER``. The mdproviders section provides an ordered list of plugins that provide metadata provider capability. These will be consulted in the defined order. Each will have a chance (on ingress) to provide add metadata to the authenticated identity. Our example mdproviders section shows one plugin configured: "sqlproperties". The sqlproperties plugin will add information related to user properties (e.g. first name and last name) to the identity dictionary. The challengers section provides an ordered list of plugins that provide challenger capability. These will be consulted in the defined order, so the system will consult the cookie auth plugin first, then the basic auth plugin. Each will have a chance to initiate a challenge. The above configuration indicates that the redirector challenger will fire if it's a browser request, and the basic auth challenger will fire if it's not (fallback). ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/docs/examples/0000775000175000017500000000000000000000000017527 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/examples/examples.ini0000664000175000017500000000035300000000000022047 0ustar00tseavertseaver00000000000000[application:login_no_who] paste.app_factory = standalone_login_no_who:main [application:login_w_who] paste.app_factory = standalone_login:main [server:main] use = egg:PasteScript#cherrypy host = 127.0.0.1 port = 5552 numthreads = 4 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/docs/examples/hybrid/0000775000175000017500000000000000000000000021010 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1464713044.0 repoze.who-3.0.0/docs/examples/hybrid/example.py0000664000175000017500000001522400000000000023021 0ustar00tseavertseaver00000000000000""" Simple BFG application demonstrating use of repoze.who in "hybrid" mode. - repoze.who middleware intercepts and validates existing request credentials, leaving 'REMOTE_USER' in the WSGI environ if they are OK. - Application handles login / logout directly, using the repoze.who API to validate credentials and set headers. """ import logging import os import sys from StringIO import StringIO from paste.httpserver import serve from repoze.bfg.authentication import RemoteUserAuthenticationPolicy from repoze.bfg.authorization import ACLAuthorizationPolicy from repoze.bfg.configuration import Configurator from repoze.bfg.security import Allow from repoze.bfg.security import Authenticated from repoze.bfg.security import DENY_ALL from repoze.bfg.security import Everyone from repoze.who.api import get_api from repoze.who.interfaces import IChallenger from repoze.who.middleware import PluggableAuthenticationMiddleware as PAM from repoze.who.plugins.basicauth import BasicAuthPlugin from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.redirector import RedirectorPlugin from repoze.who.plugins.htpasswd import HTPasswdPlugin from repoze.who.classifiers import default_request_classifier from repoze.who.classifiers import default_challenge_decider from webob import Response from webob.exc import HTTPFound LINK = '

%(title)s

' ACTIONS = { 'root': {'url': '/', 'title': 'Root'}, 'protected': {'url': '/protected.html', 'title': 'Protected'}, 'login': {'url': '/login.html', 'title': 'Login'}, 'logout': {'url': '/logout.html', 'title': 'Logout'}, } def _actions(request): names = ['root'] if 'REMOTE_USER' in request.environ: names.append('protected') names.append('logout') else: names.append('login') return '\n'.join([LINK % ACTIONS[x] for x in names]) PAGE = """\

%(page_title)s

%(actions)s """ def unprotected(request): return Response(PAGE % {'page_title': 'Unprotected Page', 'actions': _actions(request), }) def protected(request): return Response(PAGE % {'page_title': 'protected Page', 'actions': _actions(request), }) LOGIN_FORM = """\

Log In

%(came_from)s

%(message)s

Login name:

Password:

""" def login(request): message = '' info = {} # Remember any 'came_from', for redirection on succcesful login. came_from = request.params.get('came_from') if came_from is not None: info['came_from'] = ( '' % came_from) else: info['came_from'] = '' who_api = get_api(request.environ) if 'form.login' in request.POST: # Validate credentials. creds = {} creds['login'] = request.POST['login'] creds['password'] = request.POST['password'] authenticated, headers = who_api.login(creds) if authenticated: # Redirect to 'came_from', or to root. # headers here are "remember" headers, setting the # auth_tkt cookies. return HTTPFound(location=came_from or '/', headers=headers) else: message = 'Invalid login.' else: # Forcefully forget any existing credentials. _, headers = who_api.login({}) # Headers here are "forget" headers, clearing the auth_tkt cookies. request.response_headerlist = headers if 'REMOTE_USER' in request.environ: del request.environ['REMOTE_USER'] info['message'] = message return Response(LOGIN_FORM % info) def logout(request): # Use repoze.who API to get "forget" headers. who_api = get_api(request.environ) return HTTPFound(location='/', headers=who_api.logout()) class Root(object): __acl__ = [(Allow, Authenticated, ('view_protected',)), (Allow, Everyone, ('view',)), DENY_ALL, ] def get_root(*args, **kw): return Root() if __name__ == '__main__': # Configure the BFG application ## Set up security policies, root object, etc. authentication_policy=RemoteUserAuthenticationPolicy() authorization_policy=ACLAuthorizationPolicy() config = Configurator( root_factory=get_root, default_permission='view', authentication_policy=authentication_policy, authorization_policy=authorization_policy, ) config.begin() ## Configure views config.add_view(unprotected) config.add_view(protected, 'protected.html', permission='view_protected') config.add_view(login, 'login.html') config.add_view(logout, 'logout.html') config.end() ## Create the app object. app = config.make_wsgi_app() # Configure the repoze.who middleware: ## fake .htpasswd authentication source io = StringIO() for name, password in [('admin', 'admin'), ('user', 'user')]: io.write('%s:%s\n' % (name, password)) io.seek(0) def cleartext_check(password, hashed): return password == hashed htpasswd = HTPasswdPlugin(io, cleartext_check) ## other plugins basicauth = BasicAuthPlugin('repoze.who') auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt', digest_algo="sha512") redirector = RedirectorPlugin(login_url='/login.html') redirector.classifications = {IChallenger:['browser'] } # only for browser ## group / order plugins by function identifiers = [('auth_tkt', auth_tkt), ('basicauth', basicauth)] authenticators = [('auth_tkt', auth_tkt), ('htpasswd', htpasswd)] challengers = [('redirector', redirector), ('basicauth', basicauth)] mdproviders = [] ## set up who logging, if desired log_stream = None if os.environ.get('WHO_LOG'): log_stream = sys.stdout # Wrap the middleware around the application. middleware = PAM(app, identifiers, authenticators, challengers, mdproviders, default_request_classifier, default_challenge_decider, log_stream = log_stream, log_level = logging.DEBUG ) # Serve up the WSGI stack. serve(middleware, host='0.0.0.0') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1464713044.0 repoze.who-3.0.0/docs/examples/standalone_login.py0000664000175000017500000000655700000000000023436 0ustar00tseavertseaver00000000000000# Login application for demo SSO: using the repoze.who API. from repoze.who.api import APIFactory from repoze.who.config import WhoConfig from webob import Request LOGIN_FORM_TEMPLATE = """\ Demo SSO Login

Demo SSO Login

%(message)s

""" MAX_AGE = '3600' # seconds AUTH = { 'phred': 'y4bb3d4bb4d00', 'bharney': 'b3dr0ck', } # This config would normally be in a separate file: inlined here for # didactic purposes. WHO_CONFIG = """\ [plugin:auth_tkt] # identification + authorization use = repoze.who.plugins.auth_tkt:make_plugin secret = s33kr1t cookie_name = auth_cookie secure = True include_ip = True digest_algo = sha512 [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider remote_user_key = REMOTE_USER [identifiers] plugins = auth_tkt [authenticators] plugins = auth_tkt [challengers] plugins = [mdproviders] plugins = """ # oh emacs python-mode, you disappoint me """ api_factory = None def _configure_api_factory(): global api_factory if api_factory is None: config = WhoConfig(here='/tmp') # XXX config file location config.parse(WHO_CONFIG) api_factory = APIFactory(identifiers=config.identifiers, authenticators=config.authenticators, challengers=config.challengers, mdproviders=config.mdproviders, request_classifier=config.request_classifier, challenge_decider=config.challenge_decider, ) return api_factory def _validate(login_name, password): # Your application's logic goes here return AUTH.get(login_name) == password def login(environ, start_response): request = Request(environ) message = '' if 'form.submitted' in request.POST: came_from = request.POST['came_from'] login_name = request.POST['login_name'] password = request.POST['password'] remote_addr = environ['REMOTE_ADDR'] tokens = userdata = '' if _validate(login_name, password): api = _configure_api_factory()(environ) headers = [('Location', came_from)] headers.extend(api.remember(login_name)) start_response('302 Found', headers) return [] message = 'Authentication failed' else: came_from = request.GET.get('came_from') login_name = '' body = LOGIN_FORM_TEMPLATE % {'message': message, 'came_from': came_from, 'login_name': login_name, } start_response('200 OK', []) return [body] def main(global_config, **local_config): return login ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1464713044.0 repoze.who-3.0.0/docs/examples/standalone_login_no_who.py0000664000175000017500000000611200000000000024772 0ustar00tseavertseaver00000000000000# Standalone login application for demo SSO: # N.B.: this version does *not* use repoze.who at all, but should produce # a cookie which repoze.who.plugin.authtkt can use. import datetime from paste.auth import auth_tkt from webob import Request LOGIN_FORM_TEMPLATE = """\ Demo SSO Login

Demo SSO Login

%(message)s

""" # oh emacs python-mode, you disappoint me """ # Clients have to know about these values out-of-band SECRET = 's33kr1t' COOKIE_NAME = 'auth_cookie' MAX_AGE = '3600' # seconds AUTH = { 'phred': 'y4bb3d4bb4d00', 'bharney': 'b3dr0ck', } def _validate(login_name, password): # Your application's logic goes here return AUTH.get(login_name) == password def _get_cookies(environ, value): later = (datetime.datetime.now() + datetime.timedelta(seconds=int(MAX_AGE))) # Wdy, DD-Mon-YY HH:MM:SS GMT expires = later.strftime('%a, %d %b %Y %H:%M:%S') # the Expires header is *required* at least for IE7 (IE7 does # not respect Max-Age) tail = "; Max-Age=%s; Expires=%s" % (MAX_AGE, expires) cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) wild_domain = '.' + cur_domain return [('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' % (COOKIE_NAME, value, wild_domain, tail)), ] def login(environ, start_response): request = Request(environ) message = '' if 'form.submitted' in request.POST: came_from = request.POST['came_from'] login_name = request.POST['login_name'] password = request.POST['password'] remote_addr = environ['REMOTE_ADDR'] if _validate(login_name, password): headers = [('Location', came_from)] ticket = auth_tkt.AuthTicket(SECRET, login_name, remote_addr, cookie_name=COOKIE_NAME, secure=True, digest_algo="sha512") headers = _get_cookies(environ, ticket.cookie_value()) headers.append(('Location', came_from)) start_response('302 Found', headers) return [] message = 'Authentication failed' else: came_from = request.GET.get('came_from', '') login_name = '' body = LOGIN_FORM_TEMPLATE % {'message': message, 'came_from': came_from, 'login_name': login_name, } start_response('200 OK', []) return [body] def main(global_config, **local_config): return login ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/index.rst0000664000175000017500000000405400000000000017555 0ustar00tseavertseaver00000000000000.. _index: *************************************************** :mod:`repoze.who` -- WSGI Authentication Middleware *************************************************** :Author: Chris McDonough / Tres Seaver :Version: |version| .. module:: repoze.who :synopsis: WSGI authentication middleware .. topic:: Overview :mod:`repoze.who` is an identification and authentication framework for arbitrary WSGI applications. It can be used as WSGI middleware, or as an API from within a WSGI application. :mod:`repoze.who` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but :mod:`repoze.who` is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application. It attempts to reuse implementations from ``paste.auth`` for some of its functionality. Sections ======== .. toctree:: :maxdepth: 2 narr use_cases middleware api configuration plugins Change History ============== .. toctree:: :maxdepth: 2 changes Support and Development ======================= To report bugs, use the `Repoze bug tracker `_. If you've got questions that aren't answered by this documentation, contact the `Repoze-dev maillist `_ or join the `#repoze IRC channel `_. Browse and check out tagged and trunk versions of :mod:`repoze.who` via the `Repoze github repository `_. To check out the trunk via git, use this command:: git clone https://github.com:repoze/repoze.who.git Or, if you are logged in: git clone git@github.com:repoze/repoze.who.git To find out how to become a contributor to :mod:`repoze.who`, please see the `contributor's page `_. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591131249.0 repoze.who-3.0.0/docs/middleware.rst0000644000175000017500000001333200000000000020560 0ustar00tseavertseaver00000000000000.. _using_middleware: Using :mod:`repoze.who` Middleware ================================== .. _middleware_responsibilities: Middleware Responsibilities --------------------------- :mod:`repoze.who` as middleware has one major function on ingress: it conditionally places identification and authentication information (including a ``REMOTE_USER`` value) into the WSGI environment and allows the request to continue to a downstream WSGI application. :mod:`repoze.who` as middleware has one major function on egress: it examines the headers set by the downstream application, the WSGI environment, or headers supplied by other plugins and conditionally challenges for credentials. .. _request_lifecycle: Lifecycle of a Request ---------------------- :mod:`repoze.who` performs duties both on middleware "ingress" and on middleware "egress". The following graphic outlines where it sits in the context of the request and its response: .. image:: .static/request-lifecycle.png .. _ingress_stages: Request (Ingress) Stages ++++++++++++++++++++++++ .. image:: .static/ingress.png :mod:`repoze.who` performs the following operations in the following order during middleware ingress: #. Environment Setup The middleware adds a number of keys to the WSGI environment: ``repoze.who.plugins`` A reference to the configured plugin set. ``repoze.who.logger`` A reference to the logger configured into the middleware. ``repoze.who.application`` A refererence to the "right-hand" application. The plugins consulted during request classification / identification / authentication may replace this application with another WSGI application, which will be used for the remainder of the current request. #. Request Classification The middleware hands the WSGI environment to the configured ``classifier`` plugin, which is responsible for classifying the request into a single "type". This plugin must return a single string value classifying the request, e.g., "browser", "xml-rpc", "webdav", etc. This classification may serve to filter out plugins consulted later in the request. For instance, a plugin which issued a challenge as an HTML form would be inappropriate for use in requests from an XML-RPC or WebDAV client. #. Identification Each plugin configured as an identifier for a particular class of request is called to extract identity data ("credentials") from the WSGI environment. For example, a basic auth identifier might use the ``HTTP_AUTHORIZATION`` header to find login and password information. Each configured identifier plugin is consulted in turn, and any non-None identities returned are collected into a list to be authenticated. Identifiers are also responsible for providing header information used to set and remove authentication information in the response during egress (to "remember" or "forget" the currently-authenticated user). #. Authentication The middlware consults each plugin configured as an authenticators for a particular class of request, to compare credentials extracted by the identification plugins to a given policy, or set of valid credentials. For example, an htpasswd authenticator might look in a file for a user record matching any of the extracted credentials. If it finds one, and if the password listed in the record matches the password in the identity, the userid of the user would be returned (which would be the same as the login name). Successfully-authenticated identities are "weighted", with the highest weight identity governing the remainder of the request. #. Metadata Assignment After identifying and authenticating a user, :mod:`repoze.who` consults plugins configured as metadata providers, which may augment the authenticated identity with arbitrary metadata. For example, a metadata provider plugin might add the user's first, middle and last names to the identity. A more specialized metadata provider might augment the identity with a list of role or group names assigned to the user. .. _egress_stages: Response (Egress) Stages ++++++++++++++++++++++++ :mod:`repoze.who` performs the following operations in the following order during middleware egress: #. Challenge Decision The middleare examines the WSGI environment and the status and headers returned by the downstream application to determine whether a challenge is required. Typically, only the status is used: if it starts with ``401``, a challenge is required, and the challenge decider returns True. This behavior can be replaced by configuring a different ``challenge_decider`` plugin for the middleware. If a challenge is required, the challenge decider returns True; otherwise, it returns False. #. Credentials reset, AKA "forgetting" If the challenge decider returns True, the middleware first delegates to the identifier plugin which provided the currently-authenticated identity to "forget" the identity, by adding response headers (e.g., to expire a cookie). #. Challenge The plugin then consults each of the plugins configured as challengers for the current request classification: the first plugin which returns a non-None WSGI application will be used to perform a challenge. Challenger plugins may use application-returned headers, the WSGI environment, and other items to determine what sort of operation should be performed to actuate the challenge. #. Remember The identifier plugin that the "best" set of credentials came from (if any) will be consulted to "remember" these credentials if the challenge decider returns False. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591131249.0 repoze.who-3.0.0/docs/narr.rst0000644000175000017500000000434200000000000017406 0ustar00tseavertseaver00000000000000:mod:`repoze.who` Narrative Documentation ========================================= Using :mod:`repoze.who` as WSGI Middleware ------------------------------------------ :mod:`repoze.who` was originally developed for use as authentication middleware in a WSGI pipeline, for use by applications which only needed to obtain an "authenticated user" to enforce a given security policy. See :ref:`middleware_responsibilities` for a description of this use case. Using :mod:`repoze.who` without WSGI Middleware ----------------------------------------------- Some applications might want to use a configured set of :mod:`repoze.who` plugins to do identification and authentication for a request, outside the context of using :mod:`repoze.who` middleware. For example, a performance-sensitive application might wish to defer the effort of identifying and authenticating a user until the point at which authorization is required, knowing that some code paths will not need to do the work. See :ref:`api_narrative` for a description of this use case. Mixing Middleware and API Uses ------------------------------ Some applications might use the :mod:`repoze.who` middleware for most authentication purposes, but need to participate more directly in the mechanics of identification and authorization for some portions of the application. For example, consider a system which allows users to sign up online for membership in a site: once the user completes registration, such an application might wish to log the user in transparently, and thus needs to interact with the configured :mod:`repoze.who` middleware to generate response headers, ensuring that the user's next request is properly authenticated. See :ref:`middleware_api_hybrid` for a description of this use case. Configuring :mod:`repoze.who` ----------------------------- Developers and integrators can configure :mod:`repoze.who` using either imperative Python code (see :ref:`imperative_configuration`) or using an INI-style declarative configuration file (see :ref:`declarative_configuration`). In either case, the result of the configuration will be a :class:`repoze.who.api:APIFactory` instance, complete with a request classifier, a challenge decider, and a set of plugins for each plugin interface. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1464713044.0 repoze.who-3.0.0/docs/plugins.rst0000664000175000017500000006724600000000000020143 0ustar00tseavertseaver00000000000000.. _about_plugins: About :mod:`repoze.who` Plugins =============================== Plugin Types ------------ Identifier Plugins ++++++++++++++++++ You can register a plugin as willing to act as an "identifier". An identifier examines the WSGI environment and attempts to extract credentials from the environment. These credentials are used by authenticator plugins to perform authentication. Authenticator Plugins +++++++++++++++++++++ You may register a plugin as willing to act as an "authenticator". Authenticator plugins are responsible for resolving a set of credentials provided by an identifier plugin into a user id. Typically, authenticator plugins will perform a lookup into a database or some other persistent store, check the provided credentials against the stored data, and return a user id if the credentials can be validated. The user id provided by an authenticator is eventually passed to downstream WSGI applications in the "REMOTE_USER' environment variable. Additionally, the "identity" of the user (as provided by the identifier from whence the identity came) is passed along to downstream application in the ``repoze.who.identity`` environment variable. Metadata Provider Plugins +++++++++++++++++++++++++ You may register a plugin as willing to act as a "metadata provider" (aka mdprovider). Metadata provider plugins are responsible for adding arbitrary information to the identity dictionary for consumption by downstream applications. For instance, a metadata provider plugin may add "group" information to the the identity. Challenger Plugins ++++++++++++++++++ You may register a plugin as willing to act as a "challenger". Challenger plugins are responsible for initiating a challenge to the requesting user. Challenger plugins are invoked by :mod:`repoze.who` when it decides a challenge is necessary. A challenge might consist of displaying a form or presenting the user with a basic or digest authentication dialog. .. _default_plugins: Default Plugin Implementations ------------------------------ :mod:`repoze.who` ships with a variety of default plugins that do authentication, identification, challenge and metadata provision. .. module:: repoze.who.plugins.auth_tkt .. class:: AuthTktCookiePlugin(secret [, cookie_name='auth_tkt' [, secure=False [, include_ip=False]]]) An :class:`AuthTktCookiePlugin` is an ``IIdentifier`` and ``IAuthenticator`` plugin which remembers its identity state in a client-side cookie. This plugin uses the ``paste.auth.auth_tkt``"auth ticket" protocol and is compatible with Apache's mod_auth_tkt. It should be instantiated passing a *secret*, which is used to encrypt the cookie on the client side and decrypt the cookie on the server side. The cookie name used to store the cookie value can be specified using the *cookie_name* parameter. If *secure* is False, the cookie will be sent across any HTTP or HTTPS connection; if it is True, the cookie will be sent only across an HTTPS connection. If *include_ip* is True, the ``REMOTE_ADDR`` of the WSGI environment will be placed in the cookie. Normally, using the plugin as an identifier requires also using it as an authenticator. .. note:: Using the *include_ip* setting for public-facing applications may cause problems for some users. `One study `_ reports that as many as 3% of users change their IP addresses legitimately during a session. .. note:: Plugin supports remembering user data in the cookie by saving user dict into ``identity['userdata']`` parameter of ``remember`` method. They are sent unencrypted and protected by checksum. Data will then be returned every time by ``identify``. This dict must be compatible with ``urllib.urlencode`` function (``urllib.urlparse.urlencode`` in python 3). Saving keys/values with unicode characters is supported only under python 3. .. note:: Plugin supports multiple digest algorithms. It defaults to md5 to match the default for mod_auth_tkt and paste.auth.auth_tkt. However md5 is not recommended as there are viable attacks against the hash. Any algorithm from the hashlib library can be specified, currently only sha256 and sha512 are supported by mod_auth_tkt. .. module:: repoze.who.plugins.basicauth .. class:: BasicAuthPlugin(realm) A :class:`BasicAuthPlugin` plugin is both an ``IIdentifier`` and ``IChallenger`` plugin that implements the Basic Access Authentication scheme described in :rfc:`2617`. It looks for credentials within the ``HTTP-Authorization`` header sent by browsers. It challenges by sending an ``WWW-Authenticate`` header to the browser. The single argument *realm* indicates the basic auth realm that should be sent in the ``WWW-Authenticate`` header. .. module:: repoze.who.plugins.htpasswd .. class:: HTPasswdPlugin(filename, check) A :class:`HTPasswdPlugin` is an ``IAuthenticator`` implementation which compares identity information against an Apache-style htpasswd file. The *filename* argument should be an absolute path to the htpasswd file' the *check* argument is a callable which takes two arguments: "password" and "hashed", where the "password" argument is the unencrypted password provided by the identifier plugin, and the hashed value is the value stored in the htpasswd file. If the hashed value of the password matches the hash, this callable should return True. A default implementation named ``crypt_check`` is available for use as a check function (on UNIX) as ``repoze.who.plugins.htpasswd:crypt_check``; it assumes the values in the htpasswd file are encrypted with the UNIX ``crypt`` function. .. module:: repoze.who.plugins.redirector .. class:: RedirectorPlugin(login_url, came_from_param, reason_param, reason_header) A :class:`RedirectorPlugin` is an ``IChallenger`` plugin. It redirects to a configured login URL at egress if a challenge is required . *login_url* is the URL that should be redirected to when a challenge is required. *came_from_param* is the name of an optional query string parameter: if configured, the plugin provides the current request URL in the redirected URL's query string, using the supplied parameter name. *reason_param* is the name of an optional query string parameter: if configured, and the application supplies a header matching *reason_header* (defaulting to ``X-Authorization-Failure-Reason``), the plugin includes that reason in the query string of the redirected URL, using the supplied parameter name. *reason_header* is an optional parameter overriding the default response header name (``X-Authorization-Failure-Reason``) which the plugin checks to find the application-supplied reason for the challenge. *reason_header* cannot be set unless *reason_param* is also set. .. module:: repoze.who.plugins.sql .. class:: SQLAuthenticatorPlugin(query, conn_factory, compare_fn) A :class:`SQLAuthenticatorPlugin` is an ``IAuthenticator`` implementation which compares login-password identity information against data in an arbitrary SQL database. The *query* argument should be a SQL query that returns two columns in a single row considered to be the user id and the password respectively. The SQL query should contain Python-DBAPI style substitution values for ``%(login)``, e.g. ``SELECT user_id, password FROM users WHERE login = %(login)``. The *conn_factory* argument should be a callable that returns a DBAPI database connection. The *compare_fn* argument should be a callable that accepts two arguments: ``cleartext`` and ``stored_password_hash``. It should compare the hashed version of cleartext and return True if it matches the stored password hash, otherwise it should return False. A comparison function named ``default_password_compare`` exists in the ``repoze.who.plugins.sql`` module demonstrating this. The :class:`SQLAuthenticatorPlugin`\'s ``authenticate`` method will return the user id of the user unchanged to :mod:`repoze.who`. .. class:: SQLMetadataProviderPlugin(name, query, conn_factory, filter) A :class:`SQLMetatadaProviderPlugin` is an ``IMetadataProvider`` implementation which adds arbitrary metadata to the identity on ingress using data from an arbitrary SQL database. The *name* argument should be a string. It will be used as a key in the identity dictionary. The *query* argument should be a SQL query that returns arbitrary data from the database in a form that accepts Python-binding style DBAPI arguments. It should expect that a ``__userid`` value will exist in the dictionary that is bound. The SQL query should contain Python-DBAPI style substitution values for (at least) ``%(__userid)``, e.g. ``SELECT group FROM groups WHERE user_id = %(__userid)``. The *conn_factory* argument should be a callable that returns a DBAPI database connection. The *filter* argument should be a callable that accepts the result of the DBAPI ``fetchall`` based on the SQL query. It should massage the data into something that will be set in the environment under the *name* key. Writing :mod:`repoze.who` Plugins --------------------------------- :mod:`repoze.who` can be extended arbitrarily through the creation of plugins. Plugins are of one of four types: identifier plugins, authenticator plugins, metadata provider plugins, and challenge plugins. Writing An Identifier Plugin ++++++++++++++++++++++++++++ An identifier plugin (aka an ``IIdentifier`` plugin) must do three things: extract credentials from the request and turn them into an "identity", "remember" credentials, and "forget" credentials. Here's a simple cookie identification plugin that does these three things :: class InsecureCookiePlugin(object): def __init__(self, cookie_name): self.cookie_name = cookie_name def identify(self, environ): from paste.request import get_cookies cookies = get_cookies(environ) cookie = cookies.get(self.cookie_name) if cookie is None: return None import binascii try: auth = cookie.value.decode('base64') except binascii.Error: # can't decode return None try: login, password = auth.split(':', 1) return {'login':login, 'password':password} except ValueError: # not enough values to unpack return None def remember(self, environ, identity): cookie_value = '%(login)s:%(password)s' % identity cookie_value = cookie_value.encode('base64').rstrip() from paste.request import get_cookies cookies = get_cookies(environ) existing = cookies.get(self.cookie_name) value = getattr(existing, 'value', None) if value != cookie_value: # return a Set-Cookie header set_cookie = '%s=%s; Path=/;' % (self.cookie_name, cookie_value) return [('Set-Cookie', set_cookie)] def forget(self, environ, identity): # return a expires Set-Cookie header expired = ('%s=""; Path=/; Expires=Sun, 10-May-1971 11:59:00 GMT' % self.cookie_name) return [('Set-Cookie', expired)] def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) .identify ~~~~~~~~~ The ``identify`` method of our InsecureCookiePlugin accepts a single argument "environ". This will be the WSGI environment dictionary. Our plugin attempts to grub through the cookies sent by the client, trying to find one that matches our cookie name. If it finds one that matches, it attempts to decode it and turn it into a login and a password, which it returns as values in a dictionary. This dictionary is thereafter known as an "identity". If it finds no credentials in cookies, it returns None (which is not considered an identity). More generally, the ``identify`` method of an ``IIdentifier`` plugin is called once on WSGI request "ingress", and it is expected to grub arbitrarily through the WSGI environment looking for credential information. In our above plugin, the credential information is expected to be in a cookie but credential information could be in a cookie, a form field, basic/digest auth information, a header, a WSGI environment variable set by some upstream middleware or whatever else someone might use to stash authentication information. If the plugin finds credentials in the request, it's expected to return an "identity": this must be a dictionary. The dictionary is not required to have any particular keys or value composition, although it's wise if the identification plugin looks for both a login name and a password information to return at least {'login':login_name, 'password':password}, as some authenticator plugins may depend on presence of the names "login" and "password" (e.g. the htpasswd and sql ``IAuthenticator`` plugins). If an ``IIdentifier`` plugin finds no credentials, it is expected to return None. .remember ~~~~~~~~~ If we've passed a REMOTE_USER to the WSGI application during ingress (as a result of providing an identity that could be authenticated), and the downstream application doesn't kick back with an unauthorized response, on egress we want the requesting client to "remember" the identity we provided if there's some way to do that and if he hasn't already, in order to ensure he will pass it back to us on subsequent requests without requiring another login. The remember method of an ``IIdentifier`` plugin is called for each non-unauthenticated response. It is the responsibility of the ``IIdentifier`` plugin to conditionally return HTTP headers that will cause the client to remember the credentials implied by "identity". Our InsecureCookiePlugin implements the "remember" method by returning headers which set a cookie if and only if one is not already set with the same name and value in the WSGI environment. These headers will be tacked on to the response headers provided by the downstream application during the response. When you write a remember method, most of the work involved is determining *whether or not* you need to return headers. It's typical to see remember methods that compute an "old state" and a "new state" and compare the two against each other in order to determine if headers need to be returned. In our example InsecureCookiePlugin, the "old state" is ``cookie_value`` and the "new state" is ``value``. .forget ~~~~~~~ Eventually the WSGI application we're serving will issue a "401 Unauthorized" or another status signifying that the request could not be authorized. :mod:`repoze.who` intercepts this status and calls ``IIdentifier`` plugins asking them to "forget" the credentials implied by the identity. It is the "forget" method's job at this point to return HTTP headers that will effectively clear any credentials on the requesting client implied by the "identity" argument. Our InsecureCookiePlugin implements the "forget" method by returning a header which resets the cookie that was set earlier by the remember method to one that expires in the past (on my birthday, in fact). This header will be tacked onto the response headers provided by the downstream application. Writing an Authenticator Plugin +++++++++++++++++++++++++++++++ An authenticator plugin (aka an ``IAuthenticator`` plugin) must do only one thing (on "ingress"): accept an identity and check if the identity is "good". If the identity is good, it should return a "user id". This user id may or may not be the same as the "login" provided by the user. An ``IAuthenticator`` plugin will be called for each identity found during the identification phase (there may be multiple identities for a single request, as there may be multiple ``IIdentifier`` plugins active at any given time), so it may be called multiple times in the same request. Here's a simple authenticator plugin that attempts to match an identity against ones defined in an "htpasswd" file that does just that:: class SimpleHTPasswdPlugin(object): def __init__(self, filename): self.filename = filename # IAuthenticatorPlugin def authenticate(self, environ, identity): try: login = identity['login'] password = identity['password'] except KeyError: return None f = open(self.filename, 'r') for line in f: try: username, hashed = line.rstrip().split(':', 1) except ValueError: continue if username == login: if crypt_check(password, hashed): return username return None def crypt_check(password, hashed): from crypt import crypt salt = hashed[:2] return hashed == crypt(password, salt) An ``IAuthenticator`` plugin implements one "interface" method: "authentictate". The formal specification for the arguments and return values expected from these methods are available in the ``interfaces.py`` file in :mod:`repoze.who` as the ``IAuthenticator`` interface, but let's examine this method here less formally. .authenticate ~~~~~~~~~~~~~ The ``authenticate`` method accepts two arguments: the WSGI environment and an identity. Our SimpleHTPasswdPlugin ``authenticate`` implementation grabs the login and password out of the identity and attempts to find the login in the htpasswd file. If it finds it, it compares the crypted version of the password provided by the user to the crypted version stored in the htpasswd file, and finally, if they match, it returns the login. If they do not match, it returns None. .. note:: Our plugin's ``authenticate`` method does not assume that the keys ``login`` or ``password`` exist in the identity; although it requires them to do "real work" it returns None if they are not present instead of raising an exception. This is required by the ``IAuthenticator`` interface specification. Writing a Challenger Plugin +++++++++++++++++++++++++++ A challenger plugin (aka an ``IChallenger`` plugin) must do only one thing on "egress": return a WSGI application which performs a "challenge". A WSGI application is a callable that accepts an "environ" and a "start_response" as its parameters; see "PEP 333" for further definition of what a WSGI application is. A challenge asks the user for credentials. Here's an example of a simple challenger plugin:: from paste.httpheaders import WWW_AUTHENTICATE from paste.httpexceptions import HTTPUnauthorized class BasicAuthChallengerPlugin(object): def __init__(self, realm): self.realm = realm # IChallenger def challenge(self, environ, status, app_headers, forget_headers): head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) if head[0] not in forget_headers: head = head + forget_headers return HTTPUnauthorized(headers=head) Note that the plugin implements a single "interface" method: "challenge". The formal specification for the arguments and return values expected from this method is available in the "interfaces.py" file in :mod:`repoze.who` as the ``IChallenger`` interface. This method is called when :mod:`repoze.who` determines that the application has returned an "unauthorized" response (e.g. a 401). Only one challenger will be consulted during "egress" as necessary (the first one to return a non-None response). .challenge ~~~~~~~~~~ The challenge method takes environ (the WSGI environment), 'status' (the status as set by the downstream application), the "app_headers" (headers returned by the application), and the "forget_headers" (headers returned by all participating ``IIdentifier`` plugins whom were asked to "forget" this user). Our BasicAuthChallengerPlugin takes advantage of the fact that the HTTPUnauthorized exception imported from paste.httpexceptions can be used as a WSGI application. It first makes sure that we don't repeat headers if an identification plugin has already set a "WWW-Authenticate" header like ours, then it returns an instance of HTTPUnauthorized, passing in merged headers. This will cause a basic authentication dialog to be presented to the user. Writing a Metadata Provider Plugin ++++++++++++++++++++++++++++++++++ A metadata provider plugin (aka an ``IMetadataProvider`` plugin) must do only one thing (on "ingress"): "scribble" on the identity dictionary provided to it when it is called. An ``IMetadataProvider`` plugin will be called with the final "best" identity found during the authentication phase, or not at all if no "best" identity could be authenticated. Thus, each ``IMetadataProvider`` plugin will be called exactly zero or one times during a request. Here's a simple metadata provider plugin that provides "property" information from a dictionary:: _DATA = { 'chris': {'first_name':'Chris', 'last_name':'McDonough'} , 'whit': {'first_name':'Whit', 'last_name':'Morriss'} } class SimpleMetadataProvider(object): def add_metadata(self, environ, identity): userid = identity.get('repoze.who.userid') info = _DATA.get(userid) if info is not None: identity.update(info) .add_metadata ~~~~~~~~~~~~~ Arbitrarily add information to the identity dict based in other data in the environment or identity. Our plugin adds ``first_name`` and ``last_name`` values to the identity if the userid matches ``chris`` or ``whit``. Known Plugins for :mod:`repoze.who` =================================== Plugins shipped with :mod:`repoze.who` -------------------------------------- See :ref:`default_plugins`. Deprecated plugins ------------------ The :mod:`repoze.who.deprecatedplugins` distribution bundles the following plugin implementations which were shipped with :mod:`repoze.who` prior to version 2.0a3. These plugins are deprecated, and should only be used while migrating an existing deployment to replacement versions. :class:`repoze.who.plugins.cookie.InsecureCookiePlugin` An ``IIdentifier`` plugin which stores identification information in an insecure form (the base64 value of the username and password separated by a colon) in a client-side cookie. Please use the :class:`AuthTktCookiePlugin` instead. :class:`repoze.who.plugins.form.FormPlugin` An ``IIdentifier`` and ``IChallenger`` plugin, which intercepts form POSTs to gather identification at ingress and conditionally displays a login form at egress if challenge is required. Applications should supply their own login form, and use :class:`repoze.who.api.API` to authenticate and remember users. To replace the challenger role, please use :class:`repoze.who.plugins.redirector.RedirectorPlugin`, configured with the URL of your application's login form. :class:`repoze.who.plugins.form.RedirectingFormPlugin` An ``IIdentifier`` and ``IChallenger`` plugin, which intercepts form POSTs to gather identification at ingress and conditionally redirects a login form at egress if challenge is required. Applications should supply their own login form, and use :class:`repoze.who.api.API` to authenticate and remember users. To replace the challenger role, please use :class:`repoze.who.plugins.redirector.RedirectorPlugin`, configured with the URL of your application's login form. Third-party Plugins ------------------- :class:`repoze.who.plugins.zodb.ZODBPlugin` This class implements the :class:`repoze.who.interfaces.IAuthenticator` and :class:`repoze.who.interfaces.IMetadataProvider` plugin interfaces using ZODB database lookups. See http://pypi.python.org/pypi/repoze.whoplugins.zodb/ :class:`repoze.who.plugins.ldap.LDAPAuthenticatorPlugin` This class implements the :class:`repoze.who.interfaces.IAuthenticator` plugin interface using the :mod:`python-ldap` library to query an LDAP database. See http://code.gustavonarea.net/repoze.who.plugins.ldap/ :class:`repoze.who.plugins.ldap.LDAPAttributesPlugin` This class implements the :class:`repoze.who.interfaces.IMetadataProvider` plugin interface using the :mod:`python-ldap` library to query an LDAP database. See http://code.gustavonarea.net/repoze.who.plugins.ldap/ :class:`repoze.who.plugins.friendlyform.FriendlyFormPlugin` This class implements the :class:`repoze.who.interfaces.IIdentifier` and :class:`repoze.who.interfaces.IChallenger` plugin interfaces. It is similar to :class:`repoze.who.plugins.form.RedirectingFormPlugin`, bt with with additional features: - Users are not challenged on logout, unless the referrer URL is a private one (but that’s up to the application). - Developers may define post-login and/or post-logout pages. - In the login URL, the amount of failed logins is available in the environ. It’s also increased by one on every login try. This counter will allow developers not using a post-login page to handle logins that fail/succeed. See http://code.gustavonarea.net/repoze.who-friendlyform/ :func:`repoze.who.plugins.openid.identifiers.OpenIdIdentificationPlugin` This class implements the :class:`repoze.who.interfaces.IIdentifier`, :class:`repoze.who.interfaces.IAuthenticator`, and :class:`repoze.who.interfaces.IChallenger` plugin interfaces using OpenId. See http://quantumcore.org/docs/repoze.who.plugins.openid/ :func:`repoze.who.plugins.openid.classifiers.openid_challenge_decider` This function provides the :class:`repoze.who.interfaces.IChallengeDecider` interface using OpenId. See http://quantumcore.org/docs/repoze.who.plugins.openid/ :class:`repoze.who.plugins.use_beaker.UseBeakerPlugin` This packkage provids a :class:`repoze.who.interfaces.IIdentifier` plugin using :mod:`beaker.session` cache. See http://pypi.python.org/pypi/repoze.who-use_beaker/ :class:`repoze.who.plugins.cas.main_plugin.CASChallengePlugin` This class implements the :class:`repoze.who.interfaces.IIdentifier` :class:`repoze.who.interfaces.IAuthenticator`, and :class:`repoze.who.interfaces.IChallenger` plugin interfaces using CAS. See http://pypi.python.org/pypi/repoze.who.plugins.cas :class:`repoze.who.plugins.cas.challenge_decider.my_challenge_decider` This function provides the :class:`repoze.who.interfaces.IChallengeDecider` interface using CAS. See http://pypi.python.org/pypi/repoze.who.plugins.cas/ :class:`repoze.who.plugins.recaptcha.captcha.RecaptchaPlugin` This class implements the :class:`repoze.who.interfaces.IAuthenticator` plugin interface, using the recaptch API. See http://pypi.python.org/pypi/repoze.who.plugins.recaptcha/ :class:`repoze.who.plugins.sa.SQLAlchemyUserChecker` User existence checker for :class:`repoze.who.plugins.auth_tkt.AuthTktCookiePlugin`, based on the SQLAlchemy ORM. See http://pypi.python.org/pypi/repoze.who.plugins.sa/ :class:`repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin` This class implements the :class:`repoze.who.interfaces.IAuthenticator` plugin interface, using the the SQLAlchemy ORM. See http://pypi.python.org/pypi/repoze.who.plugins.sa/ :class:`repoze.who.plugins.sa.SQLAlchemyUserMDPlugin` This class implements the :class:`repoze.who.interfaces.IMetadataProvider` plugin interface, using the the SQLAlchemy ORM. See http://pypi.python.org/pypi/repoze.who.plugins.sa/ :class:`repoze.who.plugins.formcookie.CookieRedirectingFormPlugin` This class implements the :class:`repoze.who.interfaces.IIdentifier` and :class:`repoze.who.interfaces.IChallenger` plugin interfaces, similar to :class:`repoze.who.plugins.form.RedirectingFormPlugin`. The plugin tracks the ``came_from`` URL via a cookie, rather than the query string. See http://pypi.python.org/pypi/repoze.who.plugins.formcookie/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/docs/use_cases.rst0000664000175000017500000001301700000000000020417 0ustar00tseavertseaver00000000000000:mod:`repoze.who` Use Cases =========================== How should an application interact with :mod:`repoze.who`? There are three main scenarios: Middleware-Only Use Cases ------------------------- Examples of using the :mod:`repoze.who` middleware, without explicitly using its API. Simple: Bug Tracker with ``REMOTE_USER`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This application expects the ``REMOTE_USER`` variable to be set by the middleware for authenticated requests. It allows the middleware to handle challenging the user when needed. In protected views, such as those which allow creating or following up to bug reports: - Check ``environ['REMOTE_USER']`` to get the authenticated user, and apply any application-specific policy (who is allowed to edit). - If the access check fails because the user is not yet authenticated, return an 401 Unauthorized response. - If the access check fails for authenticated users, return a 403 Forbidden response. Note that the application here doesn't depend on :mod:`repoze.who` at all: it would work identically if run behind Apache's ``mod_auth``. The ``Trac`` application works exactly this way. The middleware can be configured to suit the policy required for the site, e.g.: - challenge / identify using HTTP basic authentication - authorize via an ``.htaccces``-style file. More complex: Wiki with ``repoze.who.identity`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This application use the ``repoze.who.identity`` variable set in the WSGI environment by the middleware for authenticated requests. The application still allows the middleware to handle challenging the user when needed. The only difference from the previous example is that protected views, such as those which allow adding or editing wiki pages, can use the extra metadata stored inside ``environ['repoze.who.identity']`` (a mapping) to make authorization decisions: such metadata might include groups or roles mapped by the middleware onto the user. API-Only Use Cases ------------------ Examples of using the :mod:`repoze.who` API without its middleware. Simple: Wiki with its own login and logout views. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This application uses the :mod:`repoze.who` API to compute the authenticated user, as well as using its ``remember`` API to set headers for cookie-based authentication. In each view: - Call ``api.authenticate`` to get the authenticated user. - Show a ``login`` link for non-authenticated requests. - Show a ``logout`` link for authenticated requests. - Don't show "protected" links for non-authenticated requests. In protected views, such as those which allow adding or editing wiki pages: - Call ``api.authenticate`` to get the authenticated user; check the metadata about the user (e.g., any appropriate roles or groups) to verify access. - If the access check fails because the user is not yet authenticated, redirect to the ``login`` view, with a ``came_from`` value of the current URL. - If the access check fails for authenticated users, return a 403 Forbidden response. In the login view: - For ``GET`` requests, show the login form. - For ``POST`` requests, validate the login and password from the form. If successful, call ``api.remember``, and append the returned headers to your response, which may also contain, e.g., a ``Location`` header for a redirect to the ``came_from`` URL. In this case, there will be no authenticator plugin which knows about the login / password at all. In the logout view: - Call ``api.forget`` and append the headers to your response, which may also contain, e.g., a ``Location`` header for a redirect to the ``came_from`` URL after logging out. More complex: multiple applications with "single sign-on" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this scenario, authentication is "federated" across multiple applications, which delegate to a central "login application." This application verifies credentials from the user, and then uses headers or other tokens to communicate the verified identity to the delegating application. In the login application: - The SSO login application works just like the login view described above: the difference is that the configured identifier plugins must emit headers from ``remember`` which can be recognized by their counterparts in the other apps. In the non-login applications: - Challenge plugins here must be configured to implement the specific SSO protocol, e.g. redirect to the login app with information in the query string (other protocols might differ). - Identifer plugins must be able to "crack" / consume whatever tokens are returned by the SSO login app. - Authenticators will normally be no-ops (e.g., the ``auth_tkt`` plugin used as an authenticator). Hybrid Use Cases ---------------- Examples of using the :mod:`repoze.who` API in conjuntion with its middleware. Most complex: integrate Trac and the wiki behind SSO ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This example extends the previous one, but adds into the mix the requirement that one or more of the non-login applications (e.g., Trac) be used "off the shelf," without modifying them. Such applications can be plugged into the same SSO regime, with the addition of the :mod:``repoze.who`` middleware as an adapter to bridge the gap (e.g., to turn the SSO tokens into the ``REMOTE_USER`` required by Trac). In this scenario, the middleware would be configured identically to the API used in applications which do not need the middleware shim. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/repoze/0000775000175000017500000000000000000000000016265 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/repoze/__init__.py0000664000175000017500000000011100000000000020367 0ustar00tseavertseaver00000000000000# repoze package __import__('pkg_resources').declare_namespace(__name__) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/repoze/who/0000775000175000017500000000000000000000000017062 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/repoze/who/__init__.py0000664000175000017500000000014100000000000021167 0ustar00tseavertseaver00000000000000# repoze.who package __import__('pkg_resources').declare_namespace(__name__) #pragma NO COVERAGE ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/_auth_tkt.py0000664000175000017500000002064300000000000021423 0ustar00tseavertseaver00000000000000# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php ########################################################################## # # Copyright (c) 2005 Imaginary Landscape LLC and Contributors. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ########################################################################## """ Implementation of cookie signing as done in `mod_auth_tkt `_. mod_auth_tkt is an Apache module that looks for these signed cookies and sets ``REMOTE_USER``, ``REMOTE_USER_TOKENS`` (a comma-separated list of groups) and ``REMOTE_USER_DATA`` (arbitrary string data). This module is an alternative to the ``paste.auth.cookie`` module; it's primary benefit is compatibility with mod_auth_tkt, which in turn makes it possible to use the same authentication process with non-Python code run under Apache. """ import hashlib import http.cookies import time as time_mod import urllib.parse from repoze.who._helpers import encodestring DEFAULT_DIGEST = hashlib.md5 def _exclude_separator(separator, value, fieldname): if isinstance(value, bytes): separator = separator.encode("ascii") if separator in value: raise ValueError( "{} may not contain '{}'".format(fieldname, separator) ) class AuthTicket(object): """ This class represents an authentication token. You must pass in the shared secret, the userid, and the IP address. Optionally you can include tokens (a list of strings, representing role names), 'user_data', which is arbitrary data available for your own use in later scripts. Lastly, you can override the timestamp, cookie name, whether to secure the cookie and the digest algorithm (for details look at ``AuthTKTMiddleware``). Once you provide all the arguments, use .cookie_value() to generate the appropriate authentication ticket. .cookie() generates a Cookie object, the str() of which is the complete cookie header to be sent. CGI usage:: token = auth_tkt.AuthTick('sharedsecret', 'username', os.environ['REMOTE_ADDR'], tokens=['admin']) print('Status: 200 OK') print('Content-type: text/html') print(token.cookie()) print("") ... redirect HTML ... Webware usage:: token = auth_tkt.AuthTick('sharedsecret', 'username', self.request().environ()['REMOTE_ADDR'], tokens=['admin']) self.response().setCookie('auth_tkt', token.cookie_value()) Be careful not to do an HTTP redirect after login; use meta refresh or Javascript -- some browsers have bugs where cookies aren't saved when set on a redirect. """ def __init__(self, secret, userid, ip, tokens=(), user_data='', time=None, cookie_name='auth_tkt', secure=False, digest_algo=DEFAULT_DIGEST): self.secret = secret _exclude_separator('!', userid, "'userid'") self.userid = userid self.ip = ip for token in tokens: _exclude_separator(',', token, "'token' values") _exclude_separator('!', token, "'token' values") self.tokens = ','.join(tokens) _exclude_separator('!', user_data, "'user_data'") self.user_data = user_data if time is None: self.time = time_mod.time() else: self.time = time self.cookie_name = cookie_name self.secure = secure if isinstance(digest_algo, str): # correct specification of digest from hashlib or fail self.digest_algo = getattr(hashlib, digest_algo) else: self.digest_algo = digest_algo def digest(self): return calculate_digest( self.ip, self.time, self.secret, self.userid, self.tokens, self.user_data, self.digest_algo) def cookie_value(self): v = '%s%08x%s!' % (self.digest(), int(self.time), urllib.parse.quote(self.userid)) if self.tokens: v += self.tokens + '!' v += self.user_data return v def cookie(self): c = http.cookies.SimpleCookie() c_val = encodestring(self.cookie_value()) c_val = c_val.strip().replace('\n', '') c[self.cookie_name] = c_val c[self.cookie_name]['path'] = '/' if self.secure: c[self.cookie_name]['secure'] = 'true' return c class BadTicket(Exception): """ Exception raised when a ticket can't be parsed. If we get far enough to determine what the expected digest should have been, expected is set. This should not be shown by default, but can be useful for debugging. """ def __init__(self, msg, expected=None): self.expected = expected Exception.__init__(self, msg) def parse_ticket(secret, ticket, ip, digest_algo=DEFAULT_DIGEST): """ Parse the ticket, returning (timestamp, userid, tokens, user_data). If the ticket cannot be parsed, ``BadTicket`` will be raised with an explanation. """ if isinstance(digest_algo, str): # correct specification of digest from hashlib or fail digest_algo = getattr(hashlib, digest_algo) digest_hexa_size = digest_algo().digest_size * 2 ticket = ticket.strip('"') digest = ticket[:digest_hexa_size] try: timestamp = int(ticket[digest_hexa_size:digest_hexa_size + 8], 16) except ValueError as e: raise BadTicket('Timestamp is not a hex integer: %s' % e) try: userid, data = ticket[digest_hexa_size + 8:].split('!', 1) except ValueError: raise BadTicket('userid is not followed by !') userid = urllib.parse.unquote(userid) if '!' in data: tokens, user_data = data.split('!', 1) else: # @@: Is this the right order? tokens = '' user_data = data expected = calculate_digest(ip, timestamp, secret, userid, tokens, user_data, digest_algo) if expected != digest: raise BadTicket('Digest signature is not correct', expected=(expected, digest)) tokens = tokens.split(',') return (timestamp, userid, tokens, user_data) def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, digest_algo): secret = maybe_encode(secret) userid = maybe_encode(userid) tokens = maybe_encode(tokens) user_data = maybe_encode(user_data) digest0 = digest_algo( encode_ip_timestamp(ip, timestamp) + secret + userid + b'\0' + tokens + b'\0' + user_data).hexdigest() digest = digest_algo(maybe_encode(digest0) + secret).hexdigest() return digest if type(chr(1)) == type(b''): #pragma NO COVER Python < 3.0 def ints2bytes(ints): return b''.join(map(chr, ints)) else: #pragma NO COVER Python >= 3.0 def ints2bytes(ints): return bytes(ints) def encode_ip_timestamp(ip, timestamp): ip_chars = ints2bytes(map(int, ip.split('.'))) t = int(timestamp) ts = ((t & 0xff000000) >> 24, (t & 0xff0000) >> 16, (t & 0xff00) >> 8, t & 0xff) ts_chars = ints2bytes(ts) return ip_chars + ts_chars def maybe_encode(s, encoding='utf8'): if not isinstance(s, type(b'')): s = s.encode(encoding) return s # Original Paste AuthTktMiddleware stripped: we don't have a use for it. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/_helpers.py0000664000175000017500000000264400000000000021243 0ustar00tseavertseaver00000000000000import base64 import http.cookies import wsgiref.util import wsgiref.headers def encodestring(value): return base64.encodebytes(bytes(value, 'ascii')).decode('ascii') def REQUEST_METHOD(environ): return environ['REQUEST_METHOD'] def CONTENT_TYPE(environ): return environ.get('CONTENT_TYPE', '') def USER_AGENT(environ): return environ.get('HTTP_USER_AGENT') def AUTHORIZATION(environ): return environ.get('HTTP_AUTHORIZATION', '') def get_cookies(environ): header = environ.get('HTTP_COOKIE', '') if 'paste.cookies' in environ: cookies, check_header = environ['paste.cookies'] if check_header == header: return cookies cookies = http.cookies.SimpleCookie() try: cookies.load(header) except http.cookies.CookieError: #pragma NO COVER (can't see how to provoke this) pass environ['paste.cookies'] = (cookies, header) return cookies def construct_url(environ): return wsgiref.util.request_uri(environ) def header_value(environ, key): headers = wsgiref.headers.Headers(environ) values = headers.get(key) return values or "" def must_decode(value): if type(value) is bytes: try: return value.decode('utf-8') except UnicodeDecodeError: return value.decode('latin1') return value def must_encode(value): if type(value) is str: return value.encode('utf-8') return value ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591215668.0 repoze.who-3.0.0/repoze/who/api.py0000644000175000017500000003417600000000000020216 0ustar00tseavertseaver00000000000000from zope.interface import implementer from repoze.who.interfaces import IAPI from repoze.who.interfaces import IAPIFactory from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IMetadataProvider def get_api(environ): return environ.get('repoze.who.api') @implementer(IAPIFactory) class APIFactory(object): def __init__(self, identifiers=(), authenticators=(), challengers=(), mdproviders=(), request_classifier=None, challenge_decider=None, remote_user_key = 'REMOTE_USER', logger=None, ): self.identifiers = identifiers self.authenticators = authenticators self.challengers = challengers self.mdproviders = mdproviders self.request_classifier = request_classifier self.challenge_decider = challenge_decider self.remote_user_key = remote_user_key self.logger = logger def __call__(self, environ): """ See IAPIFactory. """ api = environ.get('repoze.who.api') if api is None: api = environ['repoze.who.api'] = API(environ, self.identifiers, self.authenticators, self.challengers, self.mdproviders, self.request_classifier, self.challenge_decider, self.remote_user_key, self.logger, ) return api def verify(plugin, iface): from zope.interface.verify import verifyObject verifyObject(iface, plugin, tentative=True) def make_registries(identifiers, authenticators, challengers, mdproviders): from zope.interface.exceptions import Invalid from zope.interface.verify import BrokenImplementation # BBB, z.i < 5.0.x interface_registry = {} name_registry = {} for supplied, iface in [ (identifiers, IIdentifier), (authenticators, IAuthenticator), (challengers, IChallenger), (mdproviders, IMetadataProvider)]: for name, value in supplied: try: verify(value, iface) except (Invalid, BrokenImplementation) as why: why = str(why) raise ValueError(str(name) + ': ' + why) L = interface_registry.setdefault(iface, []) L.append(value) name_registry[name] = value return interface_registry, name_registry def match_classification(iface, plugins, classification): result = [] for plugin in plugins: plugin_classifications = getattr(plugin, 'classifications', {}) iface_classifications = plugin_classifications.get(iface) if not iface_classifications: # good for any result.append(plugin) continue if classification in iface_classifications: result.append(plugin) return result @implementer(IAPI) class API(object): def __init__(self, environ, identifiers, authenticators, challengers, mdproviders, request_classifier, challenge_decider, remote_user_key, logger, ): self.environ = environ (self.interface_registry, self.name_registry) = make_registries(identifiers, authenticators, challengers, mdproviders) self.identifiers = identifiers self.authenticators = authenticators self.challengers = challengers self.mdproviders = mdproviders self.challenge_decider = challenge_decider self.remote_user_key = remote_user_key self.logger = logger classification = self.classification = (request_classifier and request_classifier(environ)) logger and logger.info('request classification: %s' % classification) def authenticate(self): ids = self._identify() # ids will be list of tuples: [ (IIdentifier, identity) ] if ids: auth_ids = self._authenticate(ids) # auth_ids will be a list of five-tuples in the form # ( (auth_rank, id_rank), authenticator, identifier, identity, # userid ) # # When sorted, its first element will represent the "best" # identity for this request. if auth_ids: auth_ids.sort() best = auth_ids[0] rank, authenticator, identifier, identity, userid = best identity = Identity(identity) # dont show contents at print identity['authenticator'] = authenticator identity['identifier'] = identifier # allow IMetadataProvider plugins to scribble on the identity self._add_metadata(identity) # add the identity to the environment; a downstream # application can mutate it to do an 'identity reset' # as necessary, e.g. identity['login'] = 'foo', # identity['password'] = 'bar' self.environ['repoze.who.identity'] = identity # set the REMOTE_USER self.environ[self.remote_user_key] = userid return identity self.logger and self.logger.info( 'no identities found, not authenticating') def challenge(self, status='403 Forbidden', app_headers=()): """ See IAPI. """ identity = self.environ.get('repoze.who.identity', {}) identifier = identity.get('identifier') logger = self.logger forget_headers = [] if identifier: id_forget_headers = identifier.forget(self.environ, identity) if id_forget_headers is not None: forget_headers.extend(id_forget_headers) logger and logger.info('forgetting via headers from %s: %s' % (identifier, forget_headers)) candidates = self.interface_registry.get(IChallenger, ()) logger and logger.debug('challengers registered: %s' % repr(candidates)) plugins = match_classification(IChallenger, candidates, self.classification) logger and logger.debug('challengers matched for ' 'classification "%s": %s' % (self.classification, plugins)) for plugin in plugins: app = plugin.challenge(self.environ, status, app_headers, forget_headers) if app is not None: # new WSGI application logger and logger.info( 'challenger plugin %s "challenge" returned an app' % ( plugin)) return app # signifies no challenge logger and logger.info('no challenge app returned') return None def remember(self, identity=None): """ See IAPI. """ headers = () if identity is None: identity = self.environ.get('repoze.who.identity', {}) identifier = identity.get('identifier') if identifier: got_headers = identifier.remember(self.environ, identity) if got_headers: headers = got_headers logger = self.logger logger and logger.info('remembering via headers from %s: %s' % (identifier, headers)) return headers def forget(self, identity=None): """ See IAPI. """ headers = () if identity is None: identity = self.environ.get('repoze.who.identity', {}) identifier = identity.get('identifier') if identifier: got_headers = identifier.forget(self.environ, identity) if got_headers: headers = got_headers logger = self.logger logger and logger.info('forgetting via headers from %s: %s' % (identifier, headers)) return headers def login(self, credentials, identifier_name=None): """ See IAPI. """ authenticated = identity = plugin = None headers = [] # Filter identifiers using 'identifier_name', if provided. if identifier_name is not None: identifiers = [(name, plugin) for name, plugin in self.identifiers if name == identifier_name] else: identifiers = self.identifiers # First pass: for each identifier, pretend that it was the source # of the credentials, and try to authenticate. for name, identifier in identifiers: authenticated = self._authenticate([(identifier, credentials)]) if authenticated: # and therefore can remember it rank, plugin, identifier, identity, userid = authenticated[0] break # Second pass to allow identifiers which passed on auth to participate # in remember / forget. for name, identifier in identifiers: if identity is not None: i_headers = identifier.remember(self.environ, identity) else: i_headers = identifier.forget(self.environ, None) if i_headers is not None: headers.extend(i_headers) return identity, headers def logout(self, identifier_name=None): """ See IAPI. """ authenticated = None headers = [] # Filter identifiers using 'identifier_name', if provided. if identifier_name is not None: identifiers = [(name, plugin) for name, plugin in self.identifiers if name == identifier_name] else: identifiers = self.identifiers for name, identifier in identifiers: headers.extend(identifier.forget(self.environ, None)) # we need to remove the identity for hybrid middleware/api usages to # work correctly: middleware calls ``remember`` unconditionally "on # the way out", and if an identity is found, competing login headers # will be set. if 'repoze.who.identity' in self.environ: del self.environ['repoze.who.identity'] return headers def _identify(self): """ See IAPI. """ logger = self.logger candidates = self.interface_registry.get(IIdentifier, ()) logger and self.logger.debug('identifier plugins registered: %s' % (candidates,)) plugins = match_classification(IIdentifier, candidates, self.classification) logger and self.logger.debug( 'identifier plugins matched for ' 'classification "%s": %s' % (self.classification, plugins)) results = [] for plugin in plugins: identity = plugin.identify(self.environ) if identity is not None: logger and logger.debug( 'identity returned from %s: %s' % (plugin, identity)) results.append((plugin, identity)) else: logger and logger.debug( 'no identity returned from %s (%s)' % (plugin, identity)) logger and logger.debug('identities found: %s' % (results,)) return results def _authenticate(self, identities): """ See IAPI. """ logger = self.logger candidates = self.interface_registry.get(IAuthenticator, []) logger and self.logger.debug('authenticator plugins registered: %s' % candidates) plugins = match_classification(IAuthenticator, candidates, self.classification) logger and self.logger.debug( 'authenticator plugins matched for ' 'classification "%s": %s' % (self.classification, plugins)) auth_rank = 0 results = [] for plugin in plugins: identifier_rank = 0 for identifier, identity in identities: userid = plugin.authenticate(self.environ, identity) if userid is not None: logger and logger.debug( 'userid returned from %s: "%s"' % (plugin, userid)) # stamp the identity with the userid identity['repoze.who.userid'] = userid rank = (auth_rank, identifier_rank) results.append( (rank, plugin, identifier, identity, userid) ) else: logger and logger.debug( 'no userid returned from %s: (%s)' % ( plugin, userid)) identifier_rank += 1 auth_rank += 1 logger and logger.debug('identities authenticated: %s' % (results,)) return results def _add_metadata(self, identity): """ See IAPI. """ candidates = self.interface_registry.get(IMetadataProvider, ()) plugins = match_classification(IMetadataProvider, candidates, self.classification) for plugin in plugins: plugin.add_metadata(self.environ, identity) class Identity(dict): """ dict subclass: prevent members from being rendered during print """ def __repr__(self): return '' % id(self) __str__ = __repr__ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/classifiers.py0000664000175000017500000000412600000000000021746 0ustar00tseavertseaver00000000000000from repoze.who._helpers import CONTENT_TYPE from repoze.who._helpers import REQUEST_METHOD from repoze.who._helpers import USER_AGENT from zope.interface import directlyProvides from repoze.who.interfaces import IRequestClassifier from repoze.who.interfaces import IChallengeDecider _DAV_METHODS = ( 'OPTIONS', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'LOCK', 'UNLOCK', 'TRACE', 'DELETE', 'COPY', 'MOVE' ) _DAV_USERAGENTS = ( 'Microsoft Data Access Internet Publishing Provider', 'WebDrive', 'Zope External Editor', 'WebDAVFS', 'Goliath', 'neon', 'davlib', 'wsAPI', 'Microsoft-WebDAV' ) def default_request_classifier(environ): """Return one of the following classifiers: 'dav': the request comes from a WebDAV agent. 'xmlpost': the request is a POST of XML data. 'browser': the request comes from a normal browser (default). """ request_method = REQUEST_METHOD(environ) if request_method in _DAV_METHODS: return 'dav' useragent = USER_AGENT(environ) if useragent: for agent in _DAV_USERAGENTS: if useragent.find(agent) != -1: return 'dav' if request_method == 'POST': if CONTENT_TYPE(environ).lower().startswith('text/xml'): return 'xmlpost' return 'browser' directlyProvides(default_request_classifier, IRequestClassifier) def default_challenge_decider(environ, status, headers): return status.startswith('401 ') directlyProvides(default_challenge_decider, IChallengeDecider) def passthrough_challenge_decider(environ, status, headers): """ Don't challenge for pre-challenged responses. o Assume responsese with 'WWW-Authenticate' or an HTML content type are pre-challenged. """ if not status.startswith('401 '): return False h_dict = dict(headers) if 'WWW-Authenticate' in h_dict: return False ct = h_dict.get('Content-Type') if ct is not None: return not ct.startswith('text/html') return True directlyProvides(passthrough_challenge_decider, IChallengeDecider) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/config.py0000664000175000017500000001655100000000000020711 0ustar00tseavertseaver00000000000000""" Configuration parser """ import configparser from io import StringIO import logging from pkg_resources import EntryPoint import sys import warnings from repoze.who.api import APIFactory from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallengeDecider from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IMetadataProvider from repoze.who.interfaces import IPlugin from repoze.who.interfaces import IRequestClassifier from repoze.who.middleware import PluggableAuthenticationMiddleware def _resolve(name): if name: return EntryPoint.parse('x=%s' % name).resolve() class WhoConfig: def __init__(self, here): self.here = here self.request_classifier = None self.challenge_decider = None self.plugins = {} self.identifiers = [] self.authenticators = [] self.challengers = [] self.mdproviders = [] self.remote_user_key = 'REMOTE_USER' def _makePlugin(self, name, iface, options=None): if options is None: options = {} obj = _resolve(name) if not iface.providedBy(obj): obj = obj(**options) return obj def _getPlugin(self, name, iface): obj = self.plugins.get(name) if obj is None: obj = self._makePlugin(name, iface) return obj def _parsePluginSequence(self, attr, proptext, iface): lines = proptext.split() for line in lines: if ';' in line: plugin_name, classifier = line.split(';') else: plugin_name = line classifier = None plugin = self._getPlugin(plugin_name, iface) if classifier is not None: classifications = getattr(plugin, 'classifications', None) if classifications is None: classifications = plugin.classifications = {} classifications[iface] = classifier attr.append((plugin_name, plugin)) def parse(self, text): if getattr(text, 'readline', None) is None: text = StringIO(text) cp = configparser.ConfigParser(defaults={'here': self.here}) try: cp.read_file(text) except AttributeError: #pragma NO COVER Python < 3.0 cp.readfp(text) for s_id in [x for x in cp.sections() if x.startswith('plugin:')]: plugin_id = s_id[len('plugin:'):] options = dict(cp.items(s_id)) if 'use' in options: name = options.pop('use') del options['here'] obj = self._makePlugin(name, IPlugin, options) self.plugins[plugin_id] = obj if 'general' in cp.sections(): general = dict(cp.items('general')) rc = general.get('request_classifier') if rc is not None: rc = self._getPlugin(rc, IRequestClassifier) self.request_classifier = rc cd = general.get('challenge_decider') if cd is not None: cd = self._getPlugin(cd, IChallengeDecider) self.challenge_decider = cd ru = general.get('remote_user_key') if ru is not None: self.remote_user_key = ru if 'identifiers' in cp.sections(): identifiers = dict(cp.items('identifiers')) self._parsePluginSequence(self.identifiers, identifiers['plugins'], IIdentifier, ) if 'authenticators' in cp.sections(): authenticators = dict(cp.items('authenticators')) self._parsePluginSequence(self.authenticators, authenticators['plugins'], IAuthenticator, ) if 'challengers' in cp.sections(): challengers = dict(cp.items('challengers')) self._parsePluginSequence(self.challengers, challengers['plugins'], IChallenger, ) if 'mdproviders' in cp.sections(): mdproviders = dict(cp.items('mdproviders')) self._parsePluginSequence(self.mdproviders, mdproviders['plugins'], IMetadataProvider, ) class NullHandler(logging.Handler): def emit(self, record): pass _LEVELS = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, } def make_api_factory_with_config(global_conf, config_file, remote_user_key = 'REMOTE_USER', logger=None, ): identifiers = authenticators = challengers = mdproviders = () request_classifier = None challenge_decider = None parser = WhoConfig(global_conf['here']) try: opened = open(config_file) except IOError: warnings.warn('Non-existent who config file: %s' % config_file, stacklevel=2) else: try: try: parser.parse(opened) except configparser.ParsingError: warnings.warn('Invalid who config file: %s' % config_file, stacklevel=2) else: identifiers = parser.identifiers authenticators = parser.authenticators challengers = parser.challengers mdproviders = parser.mdproviders request_classifier = parser.request_classifier challenge_decider = parser.challenge_decider finally: opened.close() return APIFactory(identifiers, authenticators, challengers, mdproviders, request_classifier, challenge_decider, remote_user_key, logger, ) def make_middleware_with_config(app, global_conf, config_file, log_file=None, log_level=None): parser = WhoConfig(global_conf['here']) with open(config_file) as f: parser.parse(f) log_stream = None if log_level is None: log_level = logging.INFO elif not isinstance(log_level, int): log_level = _LEVELS[log_level.lower()] if log_file is not None: if log_file.lower() == 'stdout': log_stream = sys.stdout else: log_stream = open(log_file, 'wb') else: log_stream = logging.getLogger('repoze.who') log_stream.addHandler(NullHandler()) log_stream.setLevel(log_level or 0) return PluggableAuthenticationMiddleware( app, parser.identifiers, parser.authenticators, parser.challengers, parser.mdproviders, parser.request_classifier, parser.challenge_decider, log_stream, log_level, parser.remote_user_key, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/repoze/who/interfaces.py0000664000175000017500000002317000000000000021562 0ustar00tseavertseaver00000000000000from zope.interface import Interface class IAPIFactory(Interface): def __call__(environ): """ environ -> IRepozeWhoAPI """ class IAPI(Interface): """ Facade for stateful invocation of underlying plugins. """ def authenticate(): """ -> {identity} o Return an authenticated identity mapping, extracted from the request environment. o If no identity can be authenticated, return None. o Identity will include at least a 'repoze.who.userid' key, as well as any keys added by metadata plugins. """ def challenge(status='403 Forbidden', app_headers=()): """ -> wsgi application o Return a WSGI application which represents a "challenge" (request for credentials) in response to the current request. """ def remember(identity=None): """ -> [headers] O Return a sequence of response headers which suffice to remember the given identity. o If 'identity' is not passed, use the identity in the environment. """ def forget(identity=None): """ -> [headers] O Return a sequence of response headers which suffice to destroy any credentials used to establish an identity. o If 'identity' is not passed, use the identity in the environment. """ def login(credentials, identifier_name=None): """ -> (identity, headers) o This is an API for browser-based application login forms. o If 'identifier_name' is passed, use it to look up the identifier; othewise, use the first configured identifier. o Attempt to authenticate 'credentials' as though the identifier had extracted them. o On success, 'identity' will be authenticated mapping, and 'headers' will be "remember" headers. o On failure, 'identity' will be None, and response_headers will be "forget" headers. """ def logout(identifier_name=None): """ -> (headers) o This is an API for browser-based application logout. o If 'identifier_name' is passed, use it to look up the identifier; othewise, use the first configured identifier. o Returned headers will be "forget" headers. """ class IPlugin(Interface): pass class IRequestClassifier(IPlugin): """ On ingress: classify a request. """ def __call__(environ): """ environ -> request classifier string This interface is responsible for returning a string value representing a request classification. o 'environ' is the WSGI environment. """ class IChallengeDecider(IPlugin): """ On egress: decide whether a challenge needs to be presented to the user. """ def __call__(environ, status, headers): """ args -> True | False o 'environ' is the WSGI environment. o 'status' is the HTTP status as returned by the downstream WSGI application. o 'headers' are the headers returned by the downstream WSGI application. This interface is responsible for returning True if a challenge needs to be presented to the user, False otherwise. """ class IIdentifier(IPlugin): """ On ingress: Extract credentials from the WSGI environment and turn them into an identity. On egress (remember): Conditionally set information in the response headers allowing the remote system to remember this identity. On egress (forget): Conditionally set information in the response headers allowing the remote system to forget this identity (during a challenge). """ def identify(environ): """ On ingress: environ -> { k1 : v1 , ... , kN : vN } | None o 'environ' is the WSGI environment. o If credentials are found, the returned identity mapping will contain an arbitrary set of key/value pairs. If the identity is based on a login and password, the environment is recommended to contain at least 'login' and 'password' keys as this provides compatibility between the plugin and existing authenticator plugins. If the identity can be 'preauthenticated' (e.g. if the userid is embedded in the identity, such as when we're using ticket-based authentication), the plugin should set the userid in the special 'repoze.who.userid' key; no authenticators will be asked to authenticate the identity thereafer. o Return None to indicate that the plugin found no appropriate credentials. o Only IIdentifier plugins which match one of the the current request's classifications will be asked to perform identification. o An identifier plugin is permitted to add a key to the environment named 'repoze.who.application', which should be an arbitrary WSGI application. If an identifier plugin does so, this application is used instead of the downstream application set up within the middleware. This feature is useful for identifier plugins which need to perform redirection to obtain credentials. If two identifier plugins add a 'repoze.who.application' WSGI application to the environment, the last one consulted will"win". """ def remember(environ, identity): """ On egress (no challenge required): args -> [ (header-name, header-value), ...] | None Return a list of headers suitable for allowing the requesting system to remember the identification information (e.g. a Set-Cookie header). Return None if no headers need to be set. These headers will be appended to any headers returned by the downstream application. """ def forget(environ, identity): """ On egress (challenge required): args -> [ (header-name, header-value), ...] | None Return a list of headers suitable for allowing the requesting system to forget the identification information (e.g. a Set-Cookie header with an expires date in the past). Return None if no headers need to be set. These headers will be included in the response provided by the challenge app. """ class IAuthenticator(IPlugin): """ On ingress: validate the identity and return a user id or None. """ def authenticate(environ, identity): """ identity -> 'userid' | None o 'environ' is the WSGI environment. o 'identity' will be a dictionary (with arbitrary keys and values). o The IAuthenticator should return a single user id (optimally a string) if the identity can be authenticated. If the identify cannot be authenticated, the IAuthenticator should return None. Each instance of a registered IAuthenticator plugin that matches the request classifier will be called N times during a single request, where N is the number of identities found by any IIdentifierPlugin instances. An authenticator must not raise an exception if it is provided an identity dictionary that it does not understand (e.g. if it presumes that 'login' and 'password' are keys in the dictionary, it should check for the existence of these keys before attempting to do anything; if they don't exist, it should return None). An authenticator is permitted to add extra keys to the 'identity' dictionary (e.g., to save metadata from a database query, rather than requiring a separate query from an IMetadataProvider plugin). """ class IChallenger(IPlugin): """ On egress: Conditionally initiate a challenge to the user to provide credentials. Only challenge plugins which match one of the the current response's classifications will be asked to perform a challenge. """ def challenge(environ, status, app_headers, forget_headers): """ args -> WSGI application or None o 'environ' is the WSGI environment. o 'status' is the status written into start_response by the downstream application. o 'app_headers' is the headers list written into start_response by the downstream application. o 'forget_headers' is a list of headers which must be passed back in the response in order to perform credentials reset (logout). These come from the 'forget' method of IIdentifier plugin used to do the request's identification. Examine the values passed in and return a WSGI application (a callable which accepts environ and start_response as its two positional arguments, ala PEP 333) which causes a challenge to be performed. Return None to forego performing a challenge. """ class IMetadataProvider(IPlugin): """On ingress: When an identity is authenticated, metadata providers may scribble on the identity dictionary arbitrarily. Return values from metadata providers are ignored. """ def add_metadata(environ, identity): """ Add metadata to the identity (which is a dictionary). One value is always guaranteed to be in the dictionary when add_metadata is called: 'repoze.who.userid', representing the user id of the identity. Availability and composition of other keys will depend on the identifier plugin which created the identity. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/middleware.py0000664000175000017500000002307000000000000021553 0ustar00tseavertseaver00000000000000from io import StringIO import logging import sys from repoze.who.api import APIFactory from repoze.who.interfaces import IChallenger _STARTED = '-- repoze.who request started (%s) --' _ENDED = '-- repoze.who request ended (%s) --' class PluggableAuthenticationMiddleware(object): def __init__(self, app, identifiers, authenticators, challengers, mdproviders, request_classifier = None, challenge_decider = None, log_stream = None, log_level = logging.INFO, remote_user_key = 'REMOTE_USER', classifier = None ): if challenge_decider is None: raise ValueError('challenge_decider is required') if request_classifier is not None and classifier is not None: raise ValueError( 'Only one of request_classifier and classifier is allowed') if request_classifier is None: if classifier is None: raise ValueError( 'Either request_classifier or classifier is required') request_classifier = classifier self.app = app logger = self.logger = None if isinstance(log_stream, logging.Logger): logger = self.logger = log_stream elif log_stream: handler = logging.StreamHandler(log_stream) fmt = '%(asctime)s %(message)s' formatter = logging.Formatter(fmt) handler.setFormatter(formatter) logger = self.logger = logging.Logger('repoze.who') logger.addHandler(handler) logger.setLevel(log_level) self.remote_user_key = remote_user_key self.api_factory = APIFactory(identifiers, authenticators, challengers, mdproviders, request_classifier, challenge_decider, remote_user_key, logger ) def __call__(self, environ, start_response): if self.remote_user_key in environ: # act as a pass through if REMOTE_USER (or whatever) is # already set return self.app(environ, start_response) api = self.api_factory(environ) environ['repoze.who.plugins'] = api.name_registry # BBB? environ['repoze.who.logger'] = self.logger environ['repoze.who.application'] = self.app logger = self.logger path_info = environ.get('PATH_INFO', None) logger and logger.info(_STARTED % path_info) api.authenticate() # identity saved in environ # allow identifier plugins to replace the downstream # application (to do redirection and unauthorized themselves # mostly) app = environ.pop('repoze.who.application') if app is not self.app: logger and logger.info( 'static downstream application replaced with %s' % app) wrapper = StartResponseWrapper(start_response) app_iter = app(environ, wrapper.wrap_start_response) # The challenge decider almost(?) always needs information from the # response. The WSGI spec (PEP 333) states that a WSGI application # must call start_response by the iterable's first iteration. If # start_response hasn't been called, we'll wrap it in a way that # triggers that call. if not wrapper.called: app_iter = wrap_generator(app_iter) if api.challenge_decider(environ, wrapper.status, wrapper.headers): logger and logger.info('challenge required') close = getattr(app_iter, 'close', _no_op) challenge_app = api.challenge(wrapper.status, wrapper.headers) if challenge_app is not None: logger and logger.info('executing challenge app') if app_iter: list(app_iter) # unwind the original app iterator # PEP 333 requires that we call the original iterator's # 'close' method, if it exists, before releasing it. close() # replace the downstream app with the challenge app app_iter = challenge_app(environ, start_response) else: logger and logger.info('configuration error: no challengers') close() raise RuntimeError('no challengers found') else: logger and logger.info('no challenge required') remember_headers = api.remember() wrapper.finish_response(remember_headers) logger and logger.info(_ENDED % path_info) return app_iter def _no_op(): pass def wrap_generator(result): """\ This function returns a generator that behaves exactly the same as the original. It's only difference is it pulls the first iteration off and caches it to trigger any immediate side effects (in a WSGI world, this ensures start_response is called). """ # PEP 333 requires that we call the original iterator's # 'close' method, if it exists, before releasing it. close = getattr(result, 'close', lambda: None) # Neat trick to pull the first iteration only. We need to do this outside # of the generator function to ensure it is called. first = marker = [] for iter in result: first = iter break # Wrapper yields the first iteration, then passes result's iterations # directly up. def wrapper(): if first is not marker: yield first for iter in result: # We'll let result's StopIteration bubble up directly. yield iter close() return wrapper() class StartResponseWrapper(object): def __init__(self, start_response): self.start_response = start_response self.status = None self.headers = [] self.exc_info = None self.buffer = StringIO() # A WSGI app may delay calling start_response until the first iteration # of its generator. We track this so we know whether or not we need to # trigger an iteration before examining the response. self.called = False def wrap_start_response(self, status, headers, exc_info=None): self.headers = headers self.status = status self.exc_info = exc_info # The response has been initiated, so we have a valid code. self.called = True return self.buffer.write def finish_response(self, extra_headers): if not extra_headers: extra_headers = [] headers = self.headers + extra_headers write = self.start_response(self.status, headers, self.exc_info) if write: self.buffer.seek(0) value = self.buffer.getvalue() if value: write(value) if hasattr(write, 'close'): write.close() def make_test_middleware(app, global_conf): """ Functionally equivalent to [plugin:redirector] use = repoze.who.plugins.redirector.RedirectorPlugin login_url = /login.html [plugin:auth_tkt] use = repoze.who.plugins.auth_tkt:AuthTktCookiePlugin secret = SEEKRIT cookie_name = oatmeal [plugin:basicauth] use = repoze.who.plugins.basicauth.BasicAuthPlugin realm = repoze.who [plugin:htpasswd] use = repoze.who.plugins.htpasswd.HTPasswdPlugin filename = <...> check_fn = repoze.who.plugins.htpasswd:crypt_check [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] plugins = authtkt basicauth [authenticators] plugins = authtkt htpasswd [challengers] plugins = redirector:browser basicauth """ # be able to test without a config file from repoze.who.plugins.basicauth import BasicAuthPlugin from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.redirector import RedirectorPlugin from repoze.who.plugins.htpasswd import HTPasswdPlugin io = StringIO() for name, password in [ ('admin', 'admin'), ('chris', 'chris') ]: io.write('%s:%s\n' % (name, password)) io.seek(0) def cleartext_check(password, hashed): return password == hashed #pragma NO COVERAGE htpasswd = HTPasswdPlugin(io, cleartext_check) basicauth = BasicAuthPlugin('repoze.who') auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt') redirector = RedirectorPlugin('/login.html') redirector.classifications = {IChallenger: ['browser']} # only for browser identifiers = [('auth_tkt', auth_tkt), ('basicauth', basicauth), ] authenticators = [('htpasswd', htpasswd)] challengers = [('redirector', redirector), ('basicauth', basicauth)] mdproviders = [] from repoze.who.classifiers import default_request_classifier from repoze.who.classifiers import default_challenge_decider log_stream = None import os if os.environ.get('WHO_LOG'): log_stream = sys.stdout middleware = PluggableAuthenticationMiddleware( app, identifiers, authenticators, challengers, mdproviders, default_request_classifier, default_challenge_decider, log_stream = log_stream, log_level = logging.DEBUG ) return middleware ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/repoze/who/plugins/0000775000175000017500000000000000000000000020543 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/repoze/who/plugins/__init__.py0000664000175000017500000000015100000000000022651 0ustar00tseavertseaver00000000000000# repoze.who.plugins package __import__('pkg_resources').declare_namespace(__name__) #pragma NO COVERAGE ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/auth_tkt.py0000664000175000017500000002266300000000000022751 0ustar00tseavertseaver00000000000000import datetime from codecs import utf_8_decode from codecs import utf_8_encode import hashlib import os import time from urllib.parse import parse_qsl from urllib.parse import urlencode from wsgiref.handlers import _monthname # Locale-independent, RFC-2616 from wsgiref.handlers import _weekdayname # Locale-independent, RFC-2616 from zope.interface import implementer from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IAuthenticator from repoze.who._helpers import get_cookies import repoze.who._auth_tkt as auth_tkt _UTCNOW = None # unit tests can replace def _utcnow(): #pragma NO COVERAGE if _UTCNOW is not None: return _UTCNOW return datetime.datetime.utcnow() @implementer(IIdentifier, IAuthenticator) class AuthTktCookiePlugin(object): userid_typename = 'userid_type' userid_type_decoders = {'int': int, 'unicode': lambda x: utf_8_decode(x)[0], } userid_type_encoders = {int: ('int', str), } try: userid_type_encoders[long] = ('int', str) except NameError: #pragma NO COVER Python >= 3.0 pass try: userid_type_encoders[unicode] = ('unicode', lambda x: utf_8_encode(x)[0]) except NameError: #pragma NO COVER Python >= 3.0 pass def __init__(self, secret, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, userid_checker=None, digest_algo=auth_tkt.DEFAULT_DIGEST, samesite=None): self.secret = secret self.cookie_name = cookie_name self.include_ip = include_ip self.secure = secure if timeout and ( (not reissue_time) or (reissue_time > timeout) ): raise ValueError('When timeout is specified, reissue_time must ' 'be set to a lower value') self.timeout = timeout self.reissue_time = reissue_time self.userid_checker = userid_checker self.digest_algo = digest_algo self.samesite = samesite # IIdentifier def identify(self, environ): cookies = get_cookies(environ) cookie = cookies.get(self.cookie_name) if cookie is None or not cookie.value: return None if self.include_ip: remote_addr = environ['REMOTE_ADDR'] else: remote_addr = '0.0.0.0' try: timestamp, userid, tokens, user_data = auth_tkt.parse_ticket( self.secret, cookie.value, remote_addr, self.digest_algo) except auth_tkt.BadTicket: return None if self.timeout and ( (timestamp + self.timeout) < time.time() ): return None user_data_dict = dict(parse_qsl(user_data)) userid_type = user_data_dict.get(self.userid_typename) if userid_type: decoder = self.userid_type_decoders.get(userid_type) if decoder: userid = decoder(userid) environ['REMOTE_USER_TOKENS'] = tokens environ['REMOTE_USER_DATA'] = user_data environ['AUTH_TYPE'] = 'cookie' identity = {} identity['timestamp'] = timestamp identity['repoze.who.plugins.auth_tkt.userid'] = userid identity['tokens'] = tokens identity['userdata'] = user_data_dict return identity # IIdentifier def forget(self, environ, identity): # return a set of expires Set-Cookie headers return self._get_cookies(environ, 'INVALID', 0) # IIdentifier def remember(self, environ, identity): if self.include_ip: remote_addr = environ['REMOTE_ADDR'] else: remote_addr = '0.0.0.0' cookies = get_cookies(environ) old_cookie = cookies.get(self.cookie_name) existing = cookies.get(self.cookie_name) old_cookie_value = getattr(existing, 'value', None) max_age = identity.get('max_age', None) timestamp, userid, tokens, userdata = None, '', (), '' if old_cookie_value: try: timestamp,userid,tokens,userdata = auth_tkt.parse_ticket( self.secret, old_cookie_value, remote_addr, self.digest_algo) except auth_tkt.BadTicket: pass tokens = tuple(tokens) who_userid = identity['repoze.who.userid'] who_tokens = tuple(identity.get('tokens', ())) who_userdata_dict = identity.get('userdata', {}) encoding_data = self.userid_type_encoders.get(type(who_userid)) if encoding_data: encoding, encoder = encoding_data who_userid = encoder(who_userid) who_userdata_dict[self.userid_typename] = encoding who_userdata = urlencode(who_userdata_dict) old_data = (userid, tokens, userdata) new_data = (who_userid, who_tokens, who_userdata) if old_data != new_data or (self.reissue_time and ( (timestamp + self.reissue_time) < time.time() )): ticket = auth_tkt.AuthTicket( self.secret, who_userid, remote_addr, tokens=who_tokens, user_data=who_userdata, cookie_name=self.cookie_name, secure=self.secure, digest_algo=self.digest_algo) new_cookie_value = ticket.cookie_value() if old_cookie_value != new_cookie_value: # return a set of Set-Cookie headers return self._get_cookies(environ, new_cookie_value, max_age) # IAuthenticator def authenticate(self, environ, identity): userid = identity.get('repoze.who.plugins.auth_tkt.userid') if userid is None: return None if self.userid_checker and not self.userid_checker(userid): return None identity['repoze.who.userid'] = userid return userid def _get_cookies(self, environ, value, max_age=None): if max_age is not None: max_age = int(max_age) later = _utcnow() + datetime.timedelta(seconds=max_age) # Wdy, DD-Mon-YY HH:MM:SS GMT expires = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( _weekdayname[later.weekday()], later.day, _monthname[later.month], later.year, later.hour, later.minute, later.second, ) # the Expires header is *required* at least for IE7 (IE7 does # not respect Max-Age) max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires) else: max_age = '' secure = '' if self.secure: secure = '; secure; HttpOnly' if self.samesite: secure += '; SameSite=%s' % self.samesite cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) cur_domain = cur_domain.split(':')[0] # drop port wild_domain = '.' + cur_domain cookies = [ ('Set-Cookie', '%s="%s"; Path=/%s%s' % ( self.cookie_name, value, max_age, secure)), ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % ( self.cookie_name, value, cur_domain, max_age, secure)), ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % ( self.cookie_name, value, wild_domain, max_age, secure)) ] return cookies def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE def _bool(value): if isinstance(value, str): return value.lower() in ('yes', 'true', '1') return value def make_plugin(secret=None, secretfile=None, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, userid_checker=None, digest_algo=auth_tkt.DEFAULT_DIGEST, ): from repoze.who.utils import resolveDotted if (secret is None and secretfile is None): raise ValueError("One of 'secret' or 'secretfile' must not be None.") if (secret is not None and secretfile is not None): raise ValueError("Specify only one of 'secret' or 'secretfile'.") if secretfile: secretfile = os.path.abspath(os.path.expanduser(secretfile)) if not os.path.exists(secretfile): raise ValueError("No such 'secretfile': %s" % secretfile) with open(secretfile) as f: secret = f.read().strip() if timeout: timeout = int(timeout) if reissue_time: reissue_time = int(reissue_time) if userid_checker is not None: userid_checker = resolveDotted(userid_checker) if isinstance(digest_algo, str): try: digest_algo = getattr(hashlib, digest_algo) except AttributeError: raise ValueError("No such 'digest_algo': %s" % digest_algo) plugin = AuthTktCookiePlugin(secret, cookie_name, _bool(secure), _bool(include_ip), timeout, reissue_time, userid_checker, digest_algo, ) return plugin ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/basicauth.py0000664000175000017500000000432200000000000023061 0ustar00tseavertseaver00000000000000import base64 import binascii from webob.exc import HTTPUnauthorized from zope.interface import implementer from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger from repoze.who._helpers import AUTHORIZATION from repoze.who._helpers import must_decode @implementer(IIdentifier, IChallenger) class BasicAuthPlugin(object): def __init__(self, realm): self.realm = realm # IIdentifier def identify(self, environ): authorization = AUTHORIZATION(environ) if type(authorization) != type(b''): # this header *must* be base64-encoded ASCII authorization = authorization.encode('ascii') try: authmeth, auth = authorization.split(b' ', 1) except ValueError: # not enough values to unpack return None if authmeth.lower() == b'basic': try: auth = auth.strip() auth = base64.decodebytes(auth) except binascii.Error: # can't decode return None try: login, password = auth.split(b':', 1) except ValueError: # not enough values to unpack return None auth = {'login': must_decode(login), 'password': must_decode(password)} return auth return None # IIdentifier def remember(self, environ, identity): # we need to do nothing here; the browser remembers the basic # auth info as a result of the user typing it in. pass def _get_wwwauth(self): head = [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)] return head # IIdentifier def forget(self, environ, identity): return self._get_wwwauth() # IChallenger def challenge(self, environ, status, app_headers, forget_headers): head = self._get_wwwauth() if head[0] not in forget_headers: head = head + forget_headers return HTTPUnauthorized(headers=head) def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE def make_plugin(realm='basic'): plugin = BasicAuthPlugin(realm) return plugin ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/htpasswd.py0000664000175000017500000000722200000000000022755 0ustar00tseavertseaver00000000000000import itertools from zope.interface import implementer from repoze.who.interfaces import IAuthenticator from repoze.who.utils import resolveDotted def _padding_for_file_lines(): yield 'aaaaaa:bbbbbb' @implementer(IAuthenticator) class HTPasswdPlugin(object): def __init__(self, filename, check): self.filename = filename self.check = check # IAuthenticatorPlugin def authenticate(self, environ, identity): # NOW HEAR THIS!!! # # This method is *intentionally* slower than would be ideal because # it is trying to avoid leaking information via timing attacks # (number of users, length of user IDs, length of passwords, etc.). # # Do *not* try to optimize anything away here. try: login = identity['login'] password = identity['password'] except KeyError: return None if hasattr(self.filename, 'seek'): # assumed to have a readline self.filename.seek(0) f = self.filename must_close = False else: try: f = open(self.filename, 'r') must_close = True except IOError: environ['repoze.who.logger'].warn('could not open htpasswd ' 'file %s' % self.filename) return None result = None maybe_user = None to_check = 'ABCDEF0123456789' # Try not to reveal how many users we have. # XXX: the max count here should be configurable ;( lines = itertools.chain(f, _padding_for_file_lines()) for line in itertools.islice(lines, 0, 1000): try: username, hashed = line.rstrip().split(':', 1) except ValueError: continue if _same_string(username, login): # Don't bail early: leaks information!! maybe_user = username to_check = hashed if must_close: f.close() # Check *something* here, to mitigate a timing attack. password_ok = self.check(password, to_check) # Check our flags: if both are OK, we found a match. if password_ok and maybe_user: result = maybe_user return result def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE PADDING = ' ' * 1000 def _same_string(x, y): # Attempt at isochronous string comparison. mismatches = filter(None, [a != b for a, b, ignored in itertools.zip_longest(x, y, PADDING)]) if type(mismatches) != list: #pragma NO COVER Python >= 3.0 mismatches = list(mismatches) return len(mismatches) == 0 def crypt_check(password, hashed): from crypt import crypt salt = hashed[:2] return _same_string(hashed, crypt(password, salt)) def sha1_check(password, hashed): from hashlib import sha1 from base64 import standard_b64encode from repoze.who._helpers import must_encode b_password = must_encode(password) b_sha1_digest = sha1(b_password).digest() b_b64_sha1_digest = standard_b64encode(b_sha1_digest) return _same_string(hashed, b"{SHA}" + b_b64_sha1_digest) def plain_check(password, hashed): return _same_string(password, hashed) def make_plugin(filename=None, check_fn=None): if filename is None: raise ValueError('filename must be specified') if check_fn is None: raise ValueError('check_fn must be specified') check = resolveDotted(check_fn) return HTPasswdPlugin(filename, check) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/redirector.py0000664000175000017500000000612500000000000023263 0ustar00tseavertseaver00000000000000from urllib.parse import parse_qs from urllib.parse import urlencode from urllib.parse import urlparse from urllib.parse import urlunparse from webob.exc import HTTPFound from zope.interface import implementer from repoze.who.interfaces import IChallenger from repoze.who._helpers import construct_url from repoze.who._helpers import header_value @implementer(IChallenger) class RedirectorPlugin(object): """ Plugin for issuing challenges as redirects to a configured URL. o If the ``reason_param`` option is configured, and the application has supplied an ``X-Authorization-Failure-Reason`` header, the plugin includes that reason in the query string of the redirected URL. """ def __init__(self, login_url, came_from_param='came_from', reason_param='reason', reason_header='X-Authorization-Failure-Reason', ): self.login_url = login_url self.came_from_param = came_from_param if ((reason_param is None and reason_header is not None) or (reason_param is not None and reason_header is None)): raise ValueError( "Must supply both 'reason_header' and 'reason_param', " "or neither one.") self.reason_param = reason_param self.reason_header = reason_header self._login_url_parts = list(urlparse(login_url)) # IChallenger def challenge(self, environ, status, app_headers, forget_headers): if self.reason_param is not None or self.came_from_param is not None: url_parts = self._login_url_parts[:] query = url_parts[4] query_elements = parse_qs(query) if self.reason_param is not None: reason = header_value(app_headers, self.reason_header) if reason: query_elements[self.reason_param] = reason if self.came_from_param is not None: query_elements[self.came_from_param] = construct_url(environ) url_parts[4] = urlencode(query_elements, doseq=True) login_url = urlunparse(url_parts) else: login_url = self.login_url headers = [('Location', login_url)] + forget_headers cookies = [(h,v) for (h,v) in app_headers if h.lower() == 'set-cookie'] headers += cookies return HTTPFound(headers=headers) def make_plugin(login_url, came_from_param=None, reason_param=None, reason_header=None, ): if login_url in (u'', b'', None): raise ValueError("No 'login_url'") if reason_header is not None and reason_param is None: raise Exception("Can't set 'reason_header' without 'reason_param'.") if reason_header is None and reason_param is not None: reason_header='X-Authorization-Failure-Reason' return RedirectorPlugin(login_url, came_from_param=came_from_param, reason_param=reason_param, reason_header=reason_header, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1426707239.0 repoze.who-3.0.0/repoze/who/plugins/sql.py0000664000175000017500000001023300000000000021713 0ustar00tseavertseaver00000000000000from zope.interface import implementer from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IMetadataProvider def default_password_compare(cleartext_password, stored_password_hash): try: from hashlib import sha1 except ImportError: # Python < 2.5 #pragma NO COVERAGE from sha import new as sha1 #pragma NO COVERAGE # the stored password is stored as '{SHA}'. # or as a cleartext password (no {SHA} prefix) if stored_password_hash.startswith('{SHA}'): stored_password_hash = stored_password_hash[5:] if not isinstance(cleartext_password, type(b'')): cleartext_password = cleartext_password.encode('utf-8') digest = sha1(cleartext_password).hexdigest() else: digest = cleartext_password if stored_password_hash == digest: return True return False def make_psycopg_conn_factory(**kw): # convenience (I always seem to use Postgres) def conn_factory(): #pragma NO COVERAGE import psycopg2 #pragma NO COVERAGE return psycopg2.connect(kw['repoze.who.dsn']) #pragma NO COVERAGE return conn_factory #pragma NO COVERAGE @implementer(IAuthenticator) class SQLAuthenticatorPlugin: def __init__(self, query, conn_factory, compare_fn): # statement should be pyformat dbapi binding-style, e.g. # "select user_id, password from users where login=%(login)s" self.query = query self.conn_factory = conn_factory self.compare_fn = compare_fn or default_password_compare self.conn = None # IAuthenticator def authenticate(self, environ, identity): if not 'login' in identity: return None if not self.conn: self.conn = self.conn_factory() curs = self.conn.cursor() curs.execute(self.query, identity) result = curs.fetchone() curs.close() if result: user_id, password = result if self.compare_fn(identity['password'], password): return user_id @implementer(IMetadataProvider) class SQLMetadataProviderPlugin: def __init__(self, name, query, conn_factory, filter): self.name = name self.query = query self.conn_factory = conn_factory self.filter = filter self.conn = None # IMetadataProvider def add_metadata(self, environ, identity): if self.conn is None: self.conn = self.conn_factory() curs = self.conn.cursor() # can't use dots in names in python string formatting :-( identity['__userid'] = identity['repoze.who.userid'] curs.execute(self.query, identity) result = curs.fetchall() if self.filter: result = self.filter(result) curs.close() del identity['__userid'] identity[self.name] = result def make_authenticator_plugin(query=None, conn_factory=None, compare_fn=None, **kw): from repoze.who.utils import resolveDotted if query is None: raise ValueError('query must be specified') if conn_factory is None: raise ValueError('conn_factory must be specified') try: conn_factory = resolveDotted(conn_factory)(**kw) except Exception as why: raise ValueError('conn_factory could not be resolved: %s' % why) if compare_fn is not None: compare_fn = resolveDotted(compare_fn) return SQLAuthenticatorPlugin(query, conn_factory, compare_fn) def make_metadata_plugin(name=None, query=None, conn_factory=None, filter=None, **kw): from repoze.who.utils import resolveDotted if name is None: raise ValueError('name must be specified') if query is None: raise ValueError('query must be specified') if conn_factory is None: raise ValueError('conn_factory must be specified') try: conn_factory = resolveDotted(conn_factory)(**kw) except Exception as why: raise ValueError('conn_factory could not be resolved: %s' % why) if filter is not None: filter = resolveDotted(filter) return SQLMetadataProviderPlugin(name, query, conn_factory, filter) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/repoze/who/plugins/tests/0000775000175000017500000000000000000000000021705 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/repoze/who/plugins/tests/__init__.py0000664000175000017500000000001100000000000024006 0ustar00tseavertseaver00000000000000#package ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1674658642.0135741 repoze.who-3.0.0/repoze/who/plugins/tests/fixtures/0000775000175000017500000000000000000000000023556 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/repoze/who/plugins/tests/fixtures/__init__.py0000664000175000017500000000002400000000000025663 0ustar00tseavertseaver00000000000000# this is a package ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/repoze/who/plugins/tests/fixtures/test.htpasswd0000664000175000017500000000002400000000000026310 0ustar00tseavertseaver00000000000000badline chrism:pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/tests/test_authtkt.py0000664000175000017500000010130600000000000025003 0ustar00tseavertseaver00000000000000import unittest class TestAuthTktCookiePlugin(unittest.TestCase): tempdir = None _now_testing = None def setUp(self): pass def tearDown(self): if self.tempdir is not None: import shutil shutil.rmtree(self.tempdir) if self._now_testing is not None: self._setNowTesting(self._now_testing) def _getTargetClass(self): from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin return AuthTktCookiePlugin def _makeEnviron(self, kw=None): from wsgiref.util import setup_testing_defaults environ = {} setup_testing_defaults(environ) if kw is not None: environ.update(kw) environ['REMOTE_ADDR'] = '1.1.1.1' environ['HTTP_HOST'] = 'localhost' return environ def _makeOne(self, secret='s33kr3t', *arg, **kw): plugin = self._getTargetClass()(secret, *arg, **kw) return plugin def _makeTicket(self, userid='userid', remote_addr='0.0.0.0', tokens = [], userdata='userdata', cookie_name='auth_tkt', secure=False, time=None, digest_algo="md5"): import repoze.who._auth_tkt as auth_tkt ticket = auth_tkt.AuthTicket( 'secret', userid, remote_addr, tokens=tokens, user_data=userdata, time=time, cookie_name=cookie_name, secure=secure, digest_algo=digest_algo) return ticket.cookie_value() def _setNowTesting(self, value): from repoze.who.plugins import auth_tkt auth_tkt._UTCNOW, self._now_testing = value, auth_tkt._UTCNOW def test_class_conforms_to_IIdentifier(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IIdentifier klass = self._getTargetClass() verifyClass(IIdentifier, klass) def test_instance_conforms_to_IIdentifier(self): from zope.interface.verify import verifyObject from repoze.who.interfaces import IIdentifier klass = self._getTargetClass() verifyObject(IIdentifier, self._makeOne()) def test_class_conforms_to_IAuthenticator(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator klass = self._getTargetClass() verifyClass(IAuthenticator, klass) def test_instance_conforms_to_IAuthenticator(self): from zope.interface.verify import verifyObject from repoze.who.interfaces import IAuthenticator klass = self._getTargetClass() verifyObject(IAuthenticator, self._makeOne()) def test_timeout_no_reissue(self): self.assertRaises(ValueError, self._makeOne, 'userid', timeout=1) def test_timeout_lower_than_reissue(self): self.assertRaises(ValueError, self._makeOne, 'userid', timeout=1, reissue_time=2) def test_identify_nocookie(self): plugin = self._makeOne('secret') environ = self._makeEnviron() result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_good_cookie_include_ip(self): plugin = self._makeOne('secret', include_ip=True) val = self._makeTicket(remote_addr='1.1.1.1', userdata='foo=123') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'userid') self.assertEqual(result['userdata'], {'foo': '123'}) self.assertTrue('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'foo=123') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_good_cookie_dont_include_ip(self): plugin = self._makeOne('secret', include_ip=False) val = self._makeTicket(userdata='foo=123') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'userid') self.assertEqual(result['userdata'], {'foo': '123'}) self.assertTrue('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'foo=123') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_good_cookie_int_useridtype(self): plugin = self._makeOne('secret', include_ip=False) val = self._makeTicket(userid='1', userdata='userid_type=int') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 1) self.assertEqual(result['userdata'], {'userid_type': 'int'}) self.assertTrue('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type=int') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_good_cookie_unknown_useridtype(self): plugin = self._makeOne('secret', include_ip=False) val = self._makeTicket(userid='userid', userdata='userid_type=unknown') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'userid') self.assertEqual(result['userdata'], {'userid_type':'unknown'}) self.assertTrue('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type=unknown') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_bad_cookie(self): plugin = self._makeOne('secret', include_ip=True) environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=bogus'}) result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_bad_cookie_expired(self): import time plugin = self._makeOne('secret', timeout=2, reissue_time=1) val = self._makeTicket(userid='userid', time=time.time()-3) environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_with_checker_and_existing_account(self): plugin = self._makeOne('secret', userid_checker=dummy_userid_checker) val = self._makeTicket(userid='existing', userdata='foo=123') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'existing') self.assertEqual(result['userdata'], {'foo': '123'}) self.assertTrue('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'foo=123') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_with_alternate_hash(self): plugin = self._makeOne('secret', include_ip=False, digest_algo="sha256") val = self._makeTicket(userdata='foo=123', digest_algo="sha256") md5_val = self._makeTicket(userdata='foo=123') self.assertNotEqual(val, md5_val) # md5 is 16*2 characters long, sha256 is 32*2 self.assertEqual(len(val), len(md5_val)+32) environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'userid') self.assertEqual(result['userdata'], {'foo': '123'}) self.assertTrue('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'foo=123') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_bad_cookie_with_alternate_hash(self): plugin = self._makeOne('secret', include_ip=True, digest_algo="sha256") environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=bogus'}) result = plugin.identify(environ) self.assertEqual(result, None) def test_remember_creds_same(self): plugin = self._makeOne('secret') val = self._makeTicket(userid='userid', userdata='foo=123') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':{'foo': '123'}}) self.assertEqual(result, None) def test_remember_creds_same_alternate_hash(self): plugin = self._makeOne('secret', digest_algo="sha1") val = self._makeTicket(userid='userid', userdata='foo=123', digest_algo="sha1") environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':{'foo': '123'}}) self.assertEqual(result, None) def test_remember_creds_hash_mismatch(self): plugin = self._makeOne('secret', digest_algo="sha1") old_val = self._makeTicket(userid='userid', userdata='foo=123', digest_algo="md5") new_val = self._makeTicket(userid='userid', userdata='foo=123', digest_algo="sha1") environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':{'foo': '123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost' % new_val)) def test_remember_creds_secure_alternate_hash(self): plugin = self._makeOne('secret', secure=True, digest_algo="sha512") val = self._makeTicket(userid='userid', secure=True, userdata='foo=123', digest_algo="sha512") environ = self._makeEnviron() result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':{'foo':'123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'secure; ' 'HttpOnly' % val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost; ' 'secure; HttpOnly' % val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost; ' 'secure; HttpOnly' % val)) def test_remember_creds_secure(self): plugin = self._makeOne('secret', secure=True) val = self._makeTicket(userid='userid', secure=True, userdata='foo=123') environ = self._makeEnviron() result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':{'foo':'123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'secure; ' 'HttpOnly' % val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost; ' 'secure; HttpOnly' % val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost; ' 'secure; HttpOnly' % val)) def test_remember_creds_samesite(self): plugin = self._makeOne('secret', secure=False, samesite="Strict") val = self._makeTicket(userid='userid', secure=False, userdata='foo=123') environ = self._makeEnviron() result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':{'foo':'123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'SameSite=Strict' % val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost; ' 'SameSite=Strict' % val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost; ' 'SameSite=Strict' % val)) def test_remember_creds_different(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='other', userdata='foo=123') result = plugin.remember(environ, {'repoze.who.userid':'other', 'userdata':{'foo':'123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost' % new_val)) def test_remember_creds_different_strips_port(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val, 'HTTP_HOST': 'localhost:8080', }) new_val = self._makeTicket(userid='other', userdata='foo=123') result = plugin.remember(environ, {'repoze.who.userid':'other', 'userdata':{'foo': '123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost' % new_val)) def test_remember_creds_different_include_ip(self): plugin = self._makeOne('secret', include_ip=True) old_val = self._makeTicket(userid='userid', remote_addr='1.1.1.1') environ = self._makeEnviron({'HTTP_COOKIE': 'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='other', userdata='foo=123', remote_addr='1.1.1.1') result = plugin.remember(environ, {'repoze.who.userid':'other', 'userdata':{'foo': '123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost' % new_val)) def test_remember_creds_different_bad_old_cookie(self): plugin = self._makeOne('secret') old_val = 'BOGUS' environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='other', userdata='foo=123') result = plugin.remember(environ, {'repoze.who.userid':'other', 'userdata':{'foo': '123'}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost' % new_val)) def test_remember_creds_different_with_tokens(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='userid', userdata='foo=123', tokens=['foo', 'bar'], ) result = plugin.remember(environ, {'repoze.who.userid': 'userid', 'userdata': {'foo': '123'}, 'tokens': ['foo', 'bar'], }) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost' % new_val)) def test_remember_creds_different_with_tuple_tokens(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='userid', userdata='foo=123', tokens=['foo', 'bar'], ) result = plugin.remember(environ, {'repoze.who.userid': 'userid', 'userdata': {'foo': '123'}, 'tokens': ('foo', 'bar'), }) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; ' 'Path=/; ' 'Domain=.localhost' % new_val)) def test_remember_creds_different_int_userid(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='1', userdata='userid_type=int') result = plugin.remember(environ, {'repoze.who.userid':1, 'userdata':{}}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_remember_creds_different_unicode_userid(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) userid = b'\xc2\xa9'.decode('utf-8') userdata = '' new_val = self._makeTicket(userid=userid.encode('utf-8'), userdata=userdata) result = plugin.remember(environ, {'repoze.who.userid':userid, 'userdata':{}}) self.assertEqual(type(result[0][1]), str) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_remember_creds_reissue(self): import time plugin = self._makeOne('secret', reissue_time=1) old_val = self._makeTicket(userid='userid', userdata='', time=time.time()-2) environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='userid', userdata='') result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':''}) self.assertEqual(type(result[0][1]), str) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_remember_creds_reissue_alternate_hash(self): import time plugin = self._makeOne('secret', reissue_time=1, digest_algo="sha256") old_val = self._makeTicket(userid='userid', userdata='', time=time.time()-2, digest_algo="sha256") environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='userid', userdata='', digest_algo="sha256") result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':''}) self.assertEqual(type(result[0][1]), str) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_l10n_sane_cookie_date(self): from datetime import datetime now = datetime(2009, 11, 8, 16, 15, 22) self._setNowTesting(now) plugin = self._makeOne('secret') environ = {'HTTP_HOST': 'example.com'} tkt = self._makeTicket(userid='chris', userdata='') result = plugin.remember(environ, {'repoze.who.userid': 'chris', 'max_age': '500'}) name, value = result.pop(0) self.assertEqual('Set-Cookie', name) self.assertTrue( value.endswith('; Expires=Sun, 08 Nov 2009 16:23:42 GMT')) def test_remember_max_age(self): from datetime import datetime now = datetime(2009, 11, 8, 16, 15, 22) self._setNowTesting(now) plugin = self._makeOne('secret') environ = {'HTTP_HOST': 'example.com'} tkt = self._makeTicket(userid='chris', userdata='') result = plugin.remember(environ, {'repoze.who.userid': 'chris', 'max_age': '500'}) name, value = result.pop(0) self.assertEqual('Set-Cookie', name) self.assertTrue( value.startswith('auth_tkt="%s"; Path=/; Max-Age=500' % tkt), value) self.assertTrue( value.endswith('; Expires=Sun, 08 Nov 2009 16:23:42 GMT')) name, value = result.pop(0) self.assertEqual('Set-Cookie', name) self.assertTrue( value.startswith( 'auth_tkt="%s"; Path=/; Domain=example.com; Max-Age=500' % tkt), value) self.assertTrue( value.endswith('; Expires=Sun, 08 Nov 2009 16:23:42 GMT')) name, value = result.pop(0) self.assertEqual('Set-Cookie', name) self.assertTrue( value.startswith( 'auth_tkt="%s"; Path=/; Domain=.example.com; Max-Age=500' % tkt), value) self.assertTrue( value.endswith('; Expires=Sun, 08 Nov 2009 16:23:42 GMT')) def test_forget(self): from datetime import datetime now = datetime(2009, 11, 5, 16, 15, 22) self._setNowTesting(now) plugin = self._makeOne('secret') environ = self._makeEnviron() headers = plugin.forget(environ, None) self.assertEqual(len(headers), 3) header = headers[0] name, value = header self.assertEqual(name, 'Set-Cookie') self.assertEqual(value, 'auth_tkt="INVALID"; Path=/; ' 'Max-Age=0; Expires=Thu, 05 Nov 2009 16:15:22 GMT' ) header = headers[1] name, value = header self.assertEqual(name, 'Set-Cookie') self.assertEqual(value, 'auth_tkt="INVALID"; Path=/; Domain=localhost; ' 'Max-Age=0; Expires=Thu, 05 Nov 2009 16:15:22 GMT' ) header = headers[2] name, value = header self.assertEqual(name, 'Set-Cookie') self.assertEqual(value, 'auth_tkt="INVALID"; Path=/; Domain=.localhost; ' 'Max-Age=0; Expires=Thu, 05 Nov 2009 16:15:22 GMT' ) def test_authenticate_non_auth_tkt_credentials(self): plugin = self._makeOne() self.assertEqual(plugin.authenticate(environ={}, identity={}), None) def test_authenticate_without_checker(self): plugin = self._makeOne() identity = {'repoze.who.plugins.auth_tkt.userid': 'phred'} self.assertEqual(plugin.authenticate({}, identity), 'phred') def test_authenticate_with_checker_and_non_existing_account(self): plugin = self._makeOne('secret', userid_checker=dummy_userid_checker) identity = {'repoze.who.plugins.auth_tkt.userid': 'phred'} self.assertEqual(plugin.authenticate({}, identity), None) def test_authenticate_with_checker_and_existing_account(self): plugin = self._makeOne('secret', userid_checker=dummy_userid_checker) identity = {'repoze.who.plugins.auth_tkt.userid': 'existing'} self.assertEqual(plugin.authenticate({}, identity), 'existing') def test_factory_wo_secret_wo_secretfile_raises_ValueError(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin) def test_factory_w_secret_w_secretfile_raises_ValueError(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin, 'secret', 'secretfile') def test_factory_w_bad_secretfile_raises_ValueError(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin, secretfile='nonesuch.txt') def test_factory_w_secret(self): from repoze.who.plugins.auth_tkt import make_plugin plugin = make_plugin('secret') self.assertEqual(plugin.cookie_name, 'auth_tkt') self.assertEqual(plugin.secret, 'secret') self.assertEqual(plugin.include_ip, False) self.assertEqual(plugin.secure, False) def test_factory_w_secretfile(self): import os from tempfile import mkdtemp from repoze.who.plugins.auth_tkt import make_plugin tempdir = self.tempdir = mkdtemp() path = os.path.join(tempdir, 'who.secret') secret = open(path, 'w') secret.write('s33kr1t\n') secret.flush() secret.close() plugin = make_plugin(secretfile=path) self.assertEqual(plugin.secret, 's33kr1t') def test_factory_with_timeout_and_reissue_time(self): from repoze.who.plugins.auth_tkt import make_plugin plugin = make_plugin('secret', timeout=5, reissue_time=1) self.assertEqual(plugin.timeout, 5) self.assertEqual(plugin.reissue_time, 1) def test_factory_with_userid_checker(self): from repoze.who.plugins.auth_tkt import make_plugin plugin = make_plugin( 'secret', userid_checker='repoze.who.plugins.auth_tkt:make_plugin') self.assertEqual(plugin.userid_checker, make_plugin) def test_factory_with_alternate_hash(self): from repoze.who.plugins.auth_tkt import make_plugin import hashlib plugin = make_plugin('secret', digest_algo="sha1") self.assertEqual(plugin.digest_algo, hashlib.sha1) def test_factory_with_alternate_hash_func(self): from repoze.who.plugins.auth_tkt import make_plugin import hashlib plugin = make_plugin('secret', digest_algo=hashlib.sha1) self.assertEqual(plugin.digest_algo, hashlib.sha1) def test_factory_with_bogus_hash(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin, secret="fiddly", digest_algo='foo23') def test_remember_max_age_unicode(self): plugin = self._makeOne('secret') environ = {'HTTP_HOST':'example.com'} tkt = self._makeTicket(userid='chris', userdata='') result = plugin.remember(environ, {'repoze.who.userid': 'chris', 'max_age': u'500'}) name, value = result.pop(0) self.assertEqual('Set-Cookie', name) self.assertTrue(isinstance(value, str)) self.assertTrue( value.startswith('auth_tkt="%s"; Path=/; Max-Age=500' % tkt), (value, tkt)) self.assertTrue('; Expires=' in value) name,value = result.pop(0) self.assertEqual('Set-Cookie', name) self.assertTrue( value.startswith( 'auth_tkt="%s"; Path=/; Domain=example.com; Max-Age=500' % tkt), value) self.assertTrue('; Expires=' in value) name,value = result.pop(0) self.assertEqual('Set-Cookie', name) self.assertTrue( value.startswith( 'auth_tkt="%s"; Path=/; Domain=.example.com; Max-Age=500' % tkt), value) self.assertTrue('; Expires=' in value) def dummy_userid_checker(userid): return userid == 'existing' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/tests/test_basicauth.py0000664000175000017500000001201600000000000025261 0ustar00tseavertseaver00000000000000import unittest class TestBasicAuthPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.basicauth import BasicAuthPlugin return BasicAuthPlugin def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeEnviron(self, kw=None): from wsgiref.util import setup_testing_defaults environ = {} setup_testing_defaults(environ) if kw is not None: environ.update(kw) return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IIdentifier klass = self._getTargetClass() verifyClass(IChallenger, klass) verifyClass(IIdentifier, klass) def test_challenge(self): plugin = self._makeOne('realm') environ = self._makeEnviron() result = plugin.challenge(environ, '401 Unauthorized', [], []) self.assertNotEqual(result, None) app_iter = result(environ, lambda *arg: None) items = [] for item in app_iter: items.append(item) response = b''.join(items).decode('utf-8') self.assertTrue(response.startswith('401 Unauthorized')) def test_identify_noauthinfo(self): plugin = self._makeOne('realm') environ = self._makeEnviron() creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_nonbasic(self): plugin = self._makeOne('realm') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Digest abc'}) creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_basic_badencoding(self): plugin = self._makeOne('realm') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic abc'}) creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_basic_badrepr(self): from base64 import encodebytes plugin = self._makeOne('realm') value = encodebytes(b'foo').decode('ascii') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_basic_ok(self): from base64 import encodebytes plugin = self._makeOne('realm') value = encodebytes(b'foo:bar').decode('ascii') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, {'login':'foo', 'password':'bar'}) def test_identify_basic_ok_utf8_values(self): from base64 import encodebytes LOGIN = b'b\xc3\xa2tard' PASSWD = b'l\xc3\xa0 demain' plugin = self._makeOne('realm') value = encodebytes(b':'.join((LOGIN, PASSWD))).decode('ascii') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, {'login': LOGIN.decode('utf-8'), 'password': PASSWD.decode('utf-8')}) def test_identify_basic_ok_latin1_values(self): from base64 import encodebytes LOGIN = b'b\xe2tard' PASSWD = b'l\xe0 demain' plugin = self._makeOne('realm') value = encodebytes(b':'.join((LOGIN, PASSWD))).decode('ascii') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, {'login': LOGIN.decode('latin1'), 'password': PASSWD.decode('latin1')}) def test_remember(self): plugin = self._makeOne('realm') creds = {} environ = self._makeEnviron() result = plugin.remember(environ, creds) self.assertEqual(result, None) def test_forget(self): plugin = self._makeOne('realm') creds = {'login':'foo', 'password':'password'} environ = self._makeEnviron() result = plugin.forget(environ, creds) self.assertEqual(result, [('WWW-Authenticate', 'Basic realm="realm"')] ) def test_challenge_forgetheaders_includes(self): plugin = self._makeOne('realm') creds = {'login':'foo', 'password':'password'} environ = self._makeEnviron() forget = plugin._get_wwwauth() result = plugin.challenge(environ, '401 Unauthorized', [], forget) self.assertTrue(forget[0] in result.headers.items()) def test_challenge_forgetheaders_omits(self): plugin = self._makeOne('realm') creds = {'login':'foo', 'password':'password'} environ = self._makeEnviron() forget = plugin._get_wwwauth() result = plugin.challenge(environ, '401 Unauthorized', [], []) self.assertTrue(forget[0] in result.headers.items()) def test_factory(self): from repoze.who.plugins.basicauth import make_plugin plugin = make_plugin('realm') self.assertEqual(plugin.realm, 'realm') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/tests/test_htpasswd.py0000664000175000017500000001411200000000000025152 0ustar00tseavertseaver00000000000000import unittest class TestHTPasswdPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.htpasswd import HTPasswdPlugin return HTPasswdPlugin def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeEnviron(self): environ = {} environ['wsgi.version'] = (1,0) return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator klass = self._getTargetClass() verifyClass(IAuthenticator, klass) def test_authenticate_nocreds(self): from io import StringIO io = StringIO() plugin = self._makeOne(io, None) environ = self._makeEnviron() creds = {} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) def test_authenticate_nolines(self): from io import StringIO io = StringIO() def check(password, hashed): return True plugin = self._makeOne(io, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) def test_authenticate_nousermatch(self): from io import StringIO io = StringIO('nobody:foo') def check(password, hashed): return True plugin = self._makeOne(io, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) def test_authenticate_match(self): from io import StringIO io = StringIO('chrism:pass') def check(password, hashed): return True plugin = self._makeOne(io, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, 'chrism') def test_authenticate_badline(self): from io import StringIO io = StringIO('badline\nchrism:pass') def check(password, hashed): return True plugin = self._makeOne(io, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, 'chrism') def test_authenticate_filename(self): import os here = os.path.abspath(os.path.dirname(__file__)) htpasswd = os.path.join(here, 'fixtures', 'test.htpasswd') def check(password, hashed): return True plugin = self._makeOne(htpasswd, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, 'chrism') def test_authenticate_bad_filename_logs_to_repoze_who_logger(self): import os here = os.path.abspath(os.path.dirname(__file__)) htpasswd = os.path.join(here, 'fixtures', 'test.htpasswd.nonesuch') def check(password, hashed): # pragma: no cover return True plugin = self._makeOne(htpasswd, check) environ = self._makeEnviron() class DummyLogger: warnings = [] def warn(self, msg): self.warnings.append(msg) logger = environ['repoze.who.logger'] = DummyLogger() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) self.assertEqual(len(logger.warnings), 1) self.assertTrue('could not open htpasswd' in logger.warnings[0]) def test_crypt_check(self): import sys # win32 does not have a crypt library, don't # fail here if "win32" == sys.platform: # pragma: no cover return from crypt import crypt salt = '123' hashed = crypt('password', salt) from repoze.who.plugins.htpasswd import crypt_check self.assertEqual(crypt_check('password', hashed), True) self.assertEqual(crypt_check('notpassword', hashed), False) def test_sha1_check_w_password_str(self): from base64 import standard_b64encode from hashlib import sha1 from repoze.who.plugins.htpasswd import sha1_check password = u'password' b_password = password.encode("ascii") encrypted_string = standard_b64encode(sha1(b_password).digest()) hashed = b"%s%s" % (b"{SHA}", encrypted_string) self.assertTrue(sha1_check(password, hashed)) self.assertFalse(sha1_check('notpassword', hashed)) def test_sha1_check_w_password_bytes(self): from base64 import standard_b64encode from hashlib import sha1 from repoze.who.plugins.htpasswd import sha1_check b_password = b'password' encrypted_string = standard_b64encode(sha1(b_password).digest()) hashed = b"%s%s" % (b"{SHA}", encrypted_string) self.assertTrue(sha1_check(b_password, hashed)) self.assertFalse(sha1_check(b'notpassword', hashed)) def test_plain_check(self): from repoze.who.plugins.htpasswd import plain_check self.assertTrue(plain_check('password', 'password')) self.assertFalse(plain_check('notpassword', 'password')) def test_factory_no_filename_raises(self): from repoze.who.plugins.htpasswd import make_plugin self.assertRaises(ValueError, make_plugin) def test_factory_no_check_fn_raises(self): from repoze.who.plugins.htpasswd import make_plugin self.assertRaises(ValueError, make_plugin, 'foo') def test_factory(self): from repoze.who.plugins.htpasswd import make_plugin from repoze.who.plugins.htpasswd import crypt_check plugin = make_plugin('foo', 'repoze.who.plugins.htpasswd:crypt_check') self.assertEqual(plugin.filename, 'foo') self.assertEqual(plugin.check, crypt_check) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/tests/test_redirector.py0000664000175000017500000003601200000000000025462 0ustar00tseavertseaver00000000000000import unittest class TestRedirectorPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.redirector import RedirectorPlugin return RedirectorPlugin def _makeOne(self, login_url='http://example.com/login.html', came_from_param=None, reason_param=None, reason_header=None, ): return self._getTargetClass()(login_url, came_from_param=came_from_param, reason_param=reason_param, reason_header=reason_header) def _makeEnviron(self, path_info='/', identifier=None): from io import StringIO if identifier is None: credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) content_type, body = encode_multipart_formdata() environ = {'wsgi.version': (1,0), 'wsgi.input': StringIO(body), 'wsgi.url_scheme':'http', 'SERVER_NAME': 'www.example.com', 'SERVER_PORT': '80', 'CONTENT_TYPE': content_type, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', 'repoze.who.plugins': {'cookie':identifier}, 'QUERY_STRING': 'default=1', 'PATH_INFO': path_info, } return environ def test_class_conforms_to_IChallenger(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IChallenger verifyClass(IChallenger, self._getTargetClass()) def test_instance_conforms_to_IChallenger(self): from zope.interface.verify import verifyObject from repoze.who.interfaces import IChallenger verifyObject(IChallenger, self._makeOne()) def test_ctor_w_reason_param_wo_reason_header(self): self.assertRaises(ValueError, self._makeOne, reason_param='reason', reason_header=None) def test_ctor_wo_reason_param_w_reason_header(self): self.assertRaises(ValueError, self._makeOne, reason_param=None, reason_header='X-Reason') def test_challenge(self): from urllib.parse import parse_qsl from urllib.parse import urlparse plugin = self._makeOne(came_from_param='came_from', reason_param='reason', reason_header='X-Authorization-Failure-Reason', ) environ = self._makeEnviron() app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[0][0], 'forget') self.assertEqual(sr.headers[0][1], '1') self.assertEqual(sr.headers[1][0], 'Location') url = sr.headers[1][1] parts = urlparse(url) parts_qsl = parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') headers = sr.headers self.assertEqual(sr.headers[2][0], 'Content-Length') self.assertEqual(sr.headers[2][1], '165') self.assertEqual(sr.headers[3][0], 'Content-Type') self.assertEqual(sr.headers[3][1], 'text/plain; charset=UTF-8') self.assertEqual(sr.status, '302 Found') def test_challenge_with_reason_header(self): from urllib.parse import parse_qsl from urllib.parse import urlparse plugin = self._makeOne(came_from_param='came_from', reason_param='reason', reason_header='X-Authorization-Failure-Reason', ) environ = self._makeEnviron() app = plugin.challenge( environ, '401 Unauthorized', [('X-Authorization-Failure-Reason', 'you are ugly')], [('forget', '1')]) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[1][0], 'Location') url = sr.headers[1][1] parts = urlparse(url) parts_qsl = parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 2) parts_qsl.sort() came_from_key, came_from_value = parts_qsl[0] reason_key, reason_value = parts_qsl[1] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') self.assertEqual(reason_key, 'reason') self.assertEqual(reason_value, 'you are ugly') def test_challenge_with_custom_reason_header(self): from urllib.parse import parse_qsl from urllib.parse import urlparse plugin = self._makeOne(came_from_param='came_from', reason_param='reason', reason_header='X-Custom-Auth-Failure', ) environ = self._makeEnviron() environ['came_from'] = 'http://example.com/came_from' app = plugin.challenge( environ, '401 Unauthorized', [('X-Authorization-Failure-Reason', 'you are ugly')], [('forget', '1')]) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[1][0], 'Location') url = sr.headers[1][1] parts = urlparse(url) parts_qsl = parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') def test_challenge_w_reason_no_reason_param_no_came_from_param(self): from urllib.parse import parse_qsl from urllib.parse import urlparse plugin = self._makeOne() environ = self._makeEnviron() app = plugin.challenge( environ, '401 Unauthorized', [('X-Authorization-Failure-Reason', 'you are ugly')], [('forget', '1')]) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[0][0], "forget") self.assertEqual(sr.headers[0][1], "1") self.assertEqual(sr.headers[1][0], 'Location') url = sr.headers[1][1] parts = urlparse(url) parts_qsl = parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 0) self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') def test_challenge_w_reason_no_reason_param_w_came_from_param(self): from urllib.parse import parse_qsl from urllib.parse import urlparse plugin = self._makeOne(came_from_param='came_from', ) environ = self._makeEnviron() environ['came_from'] = 'http://example.com/came_from' app = plugin.challenge( environ, '401 Unauthorized', [('X-Authorization-Failure-Reason', 'you are ugly')], [('forget', '1')]) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[1][0], 'Location') url = sr.headers[1][1] parts = urlparse(url) parts_qsl = parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') def test_challenge_with_reason_and_custom_reason_param(self): from urllib.parse import parse_qsl from urllib.parse import urlparse plugin = self._makeOne(came_from_param='came_from', reason_param='auth_failure', reason_header='X-Custom-Auth-Failure', ) environ = self._makeEnviron() app = plugin.challenge( environ, '401 Unauthorized', [('X-Authorization-Failure-Reason', 'wrong reason'), ('X-Custom-Auth-Failure', 'you are ugly')], [('forget', '1')]) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[1][0], 'Location') url = sr.headers[1][1] parts = urlparse(url) parts_qsl = parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 2) parts_qsl.sort() reason_key, reason_value = parts_qsl[0] came_from_key, came_from_value = parts_qsl[1] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') self.assertEqual(reason_key, 'auth_failure') self.assertEqual(reason_value, 'you are ugly') def test_challenge_wo_reason_w_came_from_param(self): from urllib.parse import parse_qsl from urllib.parse import urlparse plugin = self._makeOne(came_from_param='came_from') environ = self._makeEnviron() app = plugin.challenge( environ, '401 Unauthorized', [], [('forget', '1')]) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[1][0], 'Location') url = sr.headers[1][1] parts = urlparse(url) parts_qsl = parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') def test_challenge_with_setcookie_from_app(self): plugin = self._makeOne(came_from_param='came_from', reason_param='reason', reason_header='X-Authorization-Failure-Reason', ) environ = self._makeEnviron() app = plugin.challenge( environ, '401 Unauthorized', [('app', '1'), ('set-cookie','a'), ('set-cookie','b')], []) sr = DummyStartResponse() result = b''.join(app(environ, sr)).decode('ascii') self.assertTrue(result.startswith('302 Found')) self.assertEqual(sr.headers[0][0], 'set-cookie') self.assertEqual(sr.headers[0][1], 'a') self.assertEqual(sr.headers[1][0], 'set-cookie') self.assertEqual(sr.headers[1][1], 'b') class Test_make_redirecting_plugin(unittest.TestCase): def _callFUT(self, *args, **kw): from repoze.who.plugins.redirector import make_plugin return make_plugin(*args, **kw) def test_no_login_url_raises(self): self.assertRaises(ValueError, self._callFUT, None) def test_defaults(self): plugin = self._callFUT('/go_there') self.assertEqual(plugin.login_url, '/go_there') self.assertEqual(plugin.came_from_param, None) self.assertEqual(plugin.reason_param, None) self.assertEqual(plugin.reason_header, None) def test_explicit_came_from_param(self): plugin = self._callFUT('/go_there', came_from_param='whence') self.assertEqual(plugin.login_url, '/go_there') self.assertEqual(plugin.came_from_param, 'whence') self.assertEqual(plugin.reason_param, None) self.assertEqual(plugin.reason_header, None) def test_explicit_reason_param(self): plugin = self._callFUT('/go_there', reason_param='why') self.assertEqual(plugin.login_url, '/go_there') self.assertEqual(plugin.came_from_param, None) self.assertEqual(plugin.reason_param, 'why') self.assertEqual(plugin.reason_header, 'X-Authorization-Failure-Reason') def test_explicit_reason_header_param_no_reason_param_raises(self): self.assertRaises(Exception, self._callFUT, '/go_there', reason_header='X-Reason') def test_explicit_reason_header_param(self): plugin = self._callFUT('/go_there', reason_param='why', reason_header='X-Reason') self.assertEqual(plugin.login_url, '/go_there') self.assertEqual(plugin.came_from_param, None) self.assertEqual(plugin.reason_param, 'why') self.assertEqual(plugin.reason_header, 'X-Reason') class DummyIdentifier(object): forgotten = False remembered = False def __init__(self, credentials=None, remember_headers=None, forget_headers=None, replace_app=None): self.credentials = credentials self.remember_headers = remember_headers self.forget_headers = forget_headers self.replace_app = replace_app class DummyStartResponse: def __call__(self, status, headers, exc_info=None): self.status = status self.headers = headers self.exc_info = exc_info return [] def encode_multipart_formdata(): BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' CRLF = '\r\n' L = [] L.append('--' + BOUNDARY + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/plugins/tests/test_sql.py0000664000175000017500000002232300000000000024117 0ustar00tseavertseaver00000000000000import unittest class TestSQLAuthenticatorPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.sql import SQLAuthenticatorPlugin return SQLAuthenticatorPlugin def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeEnviron(self): environ = {} environ['wsgi.version'] = (1,0) return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator klass = self._getTargetClass() verifyClass(IAuthenticator, klass, tentative=True) def test_authenticate_noresults(self): dummy_factory = DummyConnectionFactory([]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_succeed) environ = self._makeEnviron() identity = {'login':'foo', 'password':'bar'} result = plugin.authenticate(environ, identity) self.assertEqual(result, None) self.assertEqual(dummy_factory.query, 'select foo from bar') self.assertEqual(dummy_factory.closed, True) def test_authenticate_comparefail(self): dummy_factory = DummyConnectionFactory([ ['userid', 'password'] ]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_fail) environ = self._makeEnviron() identity = {'login':'fred', 'password':'bar'} result = plugin.authenticate(environ, identity) self.assertEqual(result, None) self.assertEqual(dummy_factory.query, 'select foo from bar') self.assertEqual(dummy_factory.closed, True) def test_authenticate_comparesuccess(self): dummy_factory = DummyConnectionFactory([ ['userid', 'password'] ]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_succeed) environ = self._makeEnviron() identity = {'login':'fred', 'password':'bar'} result = plugin.authenticate(environ, identity) self.assertEqual(result, 'userid') self.assertEqual(dummy_factory.query, 'select foo from bar') self.assertEqual(dummy_factory.closed, True) def test_authenticate_nologin(self): dummy_factory = DummyConnectionFactory([ ['userid', 'password'] ]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_succeed) environ = self._makeEnviron() identity = {} result = plugin.authenticate(environ, identity) self.assertEqual(result, None) self.assertEqual(dummy_factory.query, None) self.assertEqual(dummy_factory.closed, False) class TestDefaultPasswordCompare(unittest.TestCase): def _getFUT(self): from repoze.who.plugins.sql import default_password_compare return default_password_compare def _get_sha_hex_digest(self, clear='password'): try: from hashlib import sha1 except ImportError: # pragma: no cover Py3k from sha import new as sha1 if not isinstance(clear, type(b'')): # pragma: no cover Py3k clear = clear.encode('utf-8') return sha1(clear).hexdigest() def test_shaprefix_success(self): stored = '{SHA}' + self._get_sha_hex_digest() compare = self._getFUT() result = compare('password', stored) self.assertEqual(result, True) def test_shaprefix_w_unicode_cleartext(self): stored = '{SHA}' + self._get_sha_hex_digest() compare = self._getFUT() result = compare(u'password', stored) self.assertEqual(result, True) def test_shaprefix_fail(self): stored = '{SHA}' + self._get_sha_hex_digest() compare = self._getFUT() result = compare('notpassword', stored) self.assertEqual(result, False) def test_noprefix_success(self): stored = 'password' compare = self._getFUT() result = compare('password', stored) self.assertEqual(result, True) def test_noprefix_fail(self): stored = 'password' compare = self._getFUT() result = compare('notpassword', stored) self.assertEqual(result, False) class TestSQLMetadataProviderPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.sql import SQLMetadataProviderPlugin return SQLMetadataProviderPlugin def _makeOne(self, *arg, **kw): klass = self._getTargetClass() return klass(*arg, **kw) def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IMetadataProvider klass = self._getTargetClass() verifyClass(IMetadataProvider, klass, tentative=True) def test_add_metadata(self): dummy_factory = DummyConnectionFactory([ [1, 2, 3] ]) def dummy_filter(results): return results plugin = self._makeOne('md', 'select foo from bar', dummy_factory, dummy_filter) environ = {} identity = {'repoze.who.userid':1} plugin.add_metadata(environ, identity) self.assertEqual(dummy_factory.closed, True) self.assertEqual(identity['md'], [ [1,2,3] ]) self.assertEqual(dummy_factory.query, 'select foo from bar') self.assertFalse('__userid' in identity) class TestMakeSQLAuthenticatorPlugin(unittest.TestCase): def _getFUT(self): from repoze.who.plugins.sql import make_authenticator_plugin return make_authenticator_plugin def test_noquery(self): f = self._getFUT() self.assertRaises(ValueError, f, None, 'conn', 'compare') def test_no_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'statement', None, 'compare') def test_bad_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'statement', 'does.not:exist', None) def test_connfactory_specd(self): f = self._getFUT() plugin = f('statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', None) self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) from repoze.who.plugins.sql import default_password_compare self.assertEqual(plugin.compare_fn, default_password_compare) def test_comparefunc_specd(self): f = self._getFUT() plugin = f('statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory') self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) self.assertEqual(plugin.compare_fn, make_dummy_connfactory) class TestMakeSQLMetadataProviderPlugin(unittest.TestCase): def _getFUT(self): from repoze.who.plugins.sql import make_metadata_plugin return make_metadata_plugin def test_no_name(self): f = self._getFUT() self.assertRaises(ValueError, f) def test_no_query(self): f = self._getFUT() self.assertRaises(ValueError, f, 'name', None, None) def test_no_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'name', 'statement', None) def test_bad_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'name', 'statement', 'does.not:exist', None) def test_connfactory_specd(self): f = self._getFUT() plugin = f('name', 'statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', None) self.assertEqual(plugin.name, 'name') self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) self.assertEqual(plugin.filter, None) def test_comparefn_specd(self): f = self._getFUT() plugin = f('name', 'statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory') self.assertEqual(plugin.name, 'name') self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) self.assertEqual(plugin.filter, make_dummy_connfactory) class DummyConnectionFactory: # acts as all of: a factory, a connection, and a cursor closed = False query = None def __init__(self, results): self.results = results def __call__(self): return self def cursor(self): return self def execute(self, query, *arg): self.query = query self.bindargs = arg def fetchall(self): return self.results def fetchone(self): if self.results: return self.results[0] return [] def close(self): self.closed = True def compare_fail(cleartext, stored): return False def compare_succeed(cleartext, stored): return True class _DummyConnFactory: pass DummyConnFactory = _DummyConnFactory() def make_dummy_connfactory(**kw): return DummyConnFactory ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/restrict.py0000664000175000017500000000213000000000000021267 0ustar00tseavertseaver00000000000000# Authorization middleware from pkg_resources import EntryPoint def authenticated_predicate(): def _predicate(environ): return 'REMOTE_USER' in environ or 'repoze.who.identity' in environ return _predicate class PredicateRestriction: def __init__(self, app, predicate, enabled=True, **kw): self.app = app self.enabled = enabled options = kw.copy() self.predicate = predicate(**options) def __call__(self, environ, start_response): if self.enabled: if not self.predicate(environ): start_response('401 Unauthorized', []) return [] return self.app(environ, start_response) def make_authenticated_restriction(app, global_config, enabled=True): return PredicateRestriction(app, authenticated_predicate, enabled) def make_predicate_restriction(app, global_config, predicate, enabled=True, **kw): if isinstance(predicate, str): predicate = EntryPoint.parse('x=%s' % predicate).resolve() return PredicateRestriction(app, predicate, enabled, **kw) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1674658642.017574 repoze.who-3.0.0/repoze/who/tests/0000775000175000017500000000000000000000000020224 5ustar00tseavertseaver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418427463.0 repoze.who-3.0.0/repoze/who/tests/__init__.py0000664000175000017500000000001100000000000022325 0ustar00tseavertseaver00000000000000#package ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/tests/test__auth_tkt.py0000664000175000017500000002777600000000000023642 0ustar00tseavertseaver00000000000000import unittest class AuthTicketTests(unittest.TestCase): def _getTargetClass(self): from repoze.who._auth_tkt import AuthTicket return AuthTicket def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test_ctor_defaults(self): import hashlib from repoze.who import _auth_tkt with _Monkey(_auth_tkt, time_mod=_Timemod): tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4') self.assertEqual(tkt.secret, 'SEEKRIT') self.assertEqual(tkt.userid, 'USERID') self.assertEqual(tkt.ip, '1.2.3.4') self.assertEqual(tkt.tokens, '') self.assertEqual(tkt.user_data, '') self.assertEqual(tkt.time, _WHEN) self.assertEqual(tkt.cookie_name, 'auth_tkt') self.assertEqual(tkt.secure, False) self.assertEqual(tkt.digest_algo, hashlib.md5) def test_ctor_w_userid_w_embedded_bang(self): tokens = ('a,b',) # cannot be safely round-tripped with self.assertRaises(ValueError) as exc: self._makeOne('SEEKRIT', 'USER!ID', '1.2.3.4') self.assertEqual(str(exc.exception), "'userid' may not contain '!'") def test_ctor_w_token_w_embedded_bang(self): tokens = ('a!b',) # cannot be safely round-tripped with self.assertRaises(ValueError) as exc: self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=tokens) self.assertEqual( str(exc.exception), "'token' values may not contain '!'" ) def test_ctor_w_token_w_embedded_comma(self): tokens = ('a,b',) # cannot be safely round-tripped with self.assertRaises(ValueError) as exc: self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=tokens) self.assertEqual( str(exc.exception), "'token' values may not contain ','" ) def test_ctor_w_user_data_w_embedded_bang(self): user_data = 'DATA!HERE' # cannot be safely round-tripped with self.assertRaises(ValueError) as exc: self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', user_data=user_data) self.assertEqual(str(exc.exception), "'user_data' may not contain '!'") def test_ctor_explicit(self): import hashlib tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'), user_data='DATA', time=_WHEN, cookie_name='oatmeal', secure=True, digest_algo=hashlib.sha512) self.assertEqual(tkt.secret, 'SEEKRIT') self.assertEqual(tkt.userid, 'USERID') self.assertEqual(tkt.ip, '1.2.3.4') self.assertEqual(tkt.tokens, 'a,b') self.assertEqual(tkt.user_data, 'DATA') self.assertEqual(tkt.time, _WHEN) self.assertEqual(tkt.cookie_name, 'oatmeal') self.assertEqual(tkt.secure, True) self.assertEqual(tkt.digest_algo, hashlib.sha512) def test_ctor_string_algorithm(self): import hashlib tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'), user_data='DATA', time=_WHEN, cookie_name='oatmeal', secure=True, digest_algo='sha1') self.assertEqual(tkt.secret, 'SEEKRIT') self.assertEqual(tkt.userid, 'USERID') self.assertEqual(tkt.ip, '1.2.3.4') self.assertEqual(tkt.tokens, 'a,b') self.assertEqual(tkt.user_data, 'DATA') self.assertEqual(tkt.time, _WHEN) self.assertEqual(tkt.cookie_name, 'oatmeal') self.assertEqual(tkt.secure, True) self.assertEqual(tkt.digest_algo, hashlib.sha1) def test_digest(self): from repoze.who._auth_tkt import calculate_digest, hashlib tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'), user_data='DATA', time=_WHEN, cookie_name='oatmeal', secure=True) digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', 'a,b', 'DATA', hashlib.md5) self.assertEqual(tkt.digest(), digest) def test_cookie_value_wo_tokens_or_userdata(self): from repoze.who._auth_tkt import calculate_digest, hashlib tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', time=_WHEN) digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', '', '', hashlib.md5) self.assertEqual(tkt.cookie_value(), '%s%08xUSERID!' % (digest, _WHEN)) def test_cookie_value_w_tokens_and_userdata(self): from repoze.who._auth_tkt import calculate_digest, hashlib tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'), user_data='DATA', time=_WHEN) digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', 'a,b', 'DATA', hashlib.md5) self.assertEqual(tkt.cookie_value(), '%s%08xUSERID!a,b!DATA' % (digest, _WHEN)) def test_cookie_not_secure_wo_tokens_or_userdata(self): from repoze.who._auth_tkt import calculate_digest, hashlib from repoze.who._helpers import encodestring tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', time=_WHEN, cookie_name='oatmeal') digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', '', '', hashlib.md5) cookie = tkt.cookie() self.assertEqual(cookie['oatmeal'].value, encodestring('%s%08xUSERID!' % (digest, _WHEN) ).strip()) self.assertEqual(cookie['oatmeal']['path'], '/') self.assertEqual(cookie['oatmeal']['secure'], '') def test_cookie_secure_w_tokens_and_userdata(self): from repoze.who._auth_tkt import calculate_digest, hashlib from repoze.who._helpers import encodestring tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'), user_data='DATA', time=_WHEN, cookie_name='oatmeal', secure=True) digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', 'a,b', 'DATA', hashlib.md5) cookie = tkt.cookie() self.assertEqual(cookie['oatmeal'].value, encodestring('%s%08xUSERID!a,b!DATA' % (digest, _WHEN) ).strip()) self.assertEqual(cookie['oatmeal']['path'], '/') self.assertEqual(cookie['oatmeal']['secure'], 'true') class BadTicketTests(unittest.TestCase): def _getTargetClass(self): from repoze.who._auth_tkt import BadTicket return BadTicket def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test_wo_expected(self): exc = self._makeOne('message') self.assertEqual(exc.args, ('message',)) self.assertEqual(exc.expected, None) def test_w_expected(self): exc = self._makeOne('message', 'foo') self.assertEqual(exc.args, ('message',)) self.assertEqual(exc.expected, 'foo') class Test_parse_ticket(unittest.TestCase): def _callFUT(self, secret='SEEKRIT', ticket=None, ip='1.2.3.4', digest="md5"): from repoze.who._auth_tkt import parse_ticket return parse_ticket(secret, ticket, ip, digest) def test_bad_timestamp(self): from repoze.who._auth_tkt import BadTicket TICKET = '12345678901234567890123456789012XXXXXXXXuserid!' try: self._callFUT(ticket=TICKET) except BadTicket as e: self.assertTrue(e.args[0].startswith( 'Timestamp is not a hex integer:')) else: # pragma: no cover self.fail('Did not raise') def test_no_bang_after_userid(self): from repoze.who._auth_tkt import BadTicket TICKET = '1234567890123456789012345678901201020304userid' try: self._callFUT(ticket=TICKET) except BadTicket as e: self.assertEqual(e.args[0], 'userid is not followed by !') else: # pragma: no cover self.fail('Did not raise') def test_wo_tokens_or_data_bad_digest(self): from repoze.who._auth_tkt import BadTicket TICKET = '1234567890123456789012345678901201020304userid!' try: self._callFUT(ticket=TICKET) except BadTicket as e: self.assertEqual(e.args[0], 'Digest signature is not correct') else: # pragma: no cover self.fail('Did not raise') def test_wo_tokens_or_data_ok_digest(self): from repoze.who._auth_tkt import calculate_digest, hashlib digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', '', '', hashlib.md5) TICKET = '%s%08xUSERID!' % (digest, _WHEN) timestamp, userid, tokens, user_data = self._callFUT(ticket=TICKET) self.assertEqual(timestamp, _WHEN) self.assertEqual(userid, 'USERID') self.assertEqual(tokens, ['']) self.assertEqual(user_data, '') def test_w_tokens_and_data_ok_digest(self): from repoze.who._auth_tkt import calculate_digest, hashlib digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', 'a,b', 'DATA', hashlib.md5) TICKET = '%s%08xUSERID!a,b!DATA' % (digest, _WHEN) timestamp, userid, tokens, user_data = self._callFUT(ticket=TICKET) self.assertEqual(timestamp, _WHEN) self.assertEqual(userid, 'USERID') self.assertEqual(tokens, ['a', 'b']) self.assertEqual(user_data, 'DATA') def test_w_tokens_and_data_ok_alternate_digest(self): from repoze.who._auth_tkt import calculate_digest, hashlib digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID', 'a,b', 'DATA', hashlib.sha256) TICKET = '%s%08xUSERID!a,b!DATA' % (digest, _WHEN) timestamp, userid, tokens, user_data = self._callFUT( ticket=TICKET, digest=hashlib.sha256) self.assertEqual(timestamp, _WHEN) self.assertEqual(userid, 'USERID') self.assertEqual(tokens, ['a', 'b']) self.assertEqual(user_data, 'DATA') class Test_helpers(unittest.TestCase): # calculate_digest is not very testable, fully exercised through callers. def test_ints_to_bytes(self): from struct import pack from repoze.who._auth_tkt import ints2bytes self.assertEqual(ints2bytes([1, 2, 3, 4]), pack('>BBBB', 1, 2, 3, 4)) def test_encode_ip_timestamp(self): from struct import pack from repoze.who._auth_tkt import encode_ip_timestamp self.assertEqual(encode_ip_timestamp('1.2.3.4', _WHEN), pack('>BBBBL', 1, 2, 3, 4, _WHEN)) def test_maybe_encode_bytes(self): from repoze.who._auth_tkt import maybe_encode foo = b'foo' self.assertTrue(maybe_encode(foo) is foo) def test_maybe_encode_native_string(self): from repoze.who._auth_tkt import maybe_encode foo = 'foo' self.assertEqual(maybe_encode(foo), b'foo') def test_maybe_encode_unicode(self): from repoze.who._auth_tkt import maybe_encode foo = u'foo' self.assertEqual(maybe_encode(foo), b'foo') _WHEN = 1234567 class _Timemod(object): @staticmethod def time(): return _WHEN class _Monkey(object): def __init__(self, module, **replacements): self.module = module self.orig = {} self.replacements = replacements def __enter__(self): for k, v in self.replacements.items(): orig = getattr(self.module, k, self) if orig is not self: self.orig[k] = orig setattr(self.module, k, v) def __exit__(self, *exc_info): for k, v in self.replacements.items(): if k in self.orig: setattr(self.module, k, self.orig[k]) else: #pragma NO COVERSGE delattr(self.module, k) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1643739916.0 repoze.who-3.0.0/repoze/who/tests/test__helpers.py0000664000175000017500000001137600000000000023446 0ustar00tseavertseaver00000000000000import unittest class CompatTests(unittest.TestCase): def test_REQUEST_METHOD_miss(self): # PEP 3333 says CONTENT_TYPE is mandatory from repoze.who._helpers import REQUEST_METHOD self.assertRaises(KeyError, REQUEST_METHOD, {}) def test_REQUEST_METHOD_hit(self): from repoze.who._helpers import REQUEST_METHOD self.assertEqual(REQUEST_METHOD({'REQUEST_METHOD': 'FOO'}), 'FOO') def test_CONTENT_TYPE_miss(self): # PEP 3333 says CONTENT_TYPE is optional from repoze.who._helpers import CONTENT_TYPE self.assertEqual(CONTENT_TYPE({}), '') def test_CONTENT_TYPE_hit(self): from repoze.who._helpers import CONTENT_TYPE self.assertEqual(CONTENT_TYPE({'CONTENT_TYPE': 'text/html'}), 'text/html') def test_USER_AGENT_miss(self): from repoze.who._helpers import USER_AGENT self.assertEqual(USER_AGENT({}), None) def test_USER_AGENT_hit(self): from repoze.who._helpers import USER_AGENT self.assertEqual(USER_AGENT({'HTTP_USER_AGENT': 'FOO'}), 'FOO') def test_AUTHORIZATION_miss(self): from repoze.who._helpers import AUTHORIZATION self.assertEqual(AUTHORIZATION({}), '') def test_AUTHORIZATION_hit(self): from repoze.who._helpers import AUTHORIZATION self.assertEqual(AUTHORIZATION({'HTTP_AUTHORIZATION': 'FOO'}), 'FOO') def test_get_cookies_no_cache_ok_header_value(self): from http.cookies import SimpleCookie from repoze.who._helpers import get_cookies environ = {'HTTP_COOKIE': 'qux=spam'} cookies = get_cookies(environ) self.assertTrue(isinstance(cookies, SimpleCookie)) self.assertEqual(len(cookies), 1) self.assertEqual(cookies['qux'].value, 'spam') self.assertEqual(environ['paste.cookies'], (cookies, 'qux=spam')) def test_get_cookies_w_cache_miss(self): from http.cookies import SimpleCookie from repoze.who._helpers import get_cookies environ = {'HTTP_COOKIE': 'qux=spam', 'paste.cookies': (object(), 'foo=bar'), } cookies = get_cookies(environ) self.assertTrue(isinstance(cookies, SimpleCookie)) self.assertEqual(len(cookies), 1) self.assertEqual(cookies['qux'].value, 'spam') self.assertEqual(environ['paste.cookies'], (cookies, 'qux=spam')) def test_get_cookies_w_cache_hit(self): from http.cookies import SimpleCookie from repoze.who._helpers import get_cookies existing = SimpleCookie() existing['foo'] = 'bar' environ = {'HTTP_COOKIE': 'qux=spam', 'paste.cookies': (existing, 'qux=spam'), } cookies = get_cookies(environ) self.assertTrue(cookies is existing) def test_construct_url(self): from repoze.who._helpers import construct_url environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } self.assertEqual(construct_url(environ), 'http://example.com/') def test_header_value_miss(self): from repoze.who._helpers import header_value self.assertEqual(header_value([], 'nonesuch'), '') def test_header_value_simple(self): from repoze.who._helpers import header_value self.assertEqual(header_value([('simple', 'SIMPLE')], 'simple'), 'SIMPLE') def test_must_decode_non_string(self): from repoze.who._helpers import must_decode foo = object() self.assertTrue(must_decode(foo) is foo) def test_must_decode_unicode(self): from repoze.who._helpers import must_decode foo = u'foo' self.assertTrue(must_decode(foo) is foo) def test_must_decode_utf8(self): from repoze.who._helpers import must_decode foo = b'b\xc3\xa2tard' self.assertEqual(must_decode(foo), foo.decode('utf-8')) def test_must_decode_latin1(self): from repoze.who._helpers import must_decode foo = b'b\xe2tard' self.assertEqual(must_decode(foo), foo.decode('latin1')) def test_must_encode_non_string(self): from repoze.who._helpers import must_encode foo = object() self.assertTrue(must_encode(foo) is foo) def test_must_encode_unicode(self): from repoze.who._helpers import must_encode foo = u'foo' self.assertEqual(must_encode(foo), foo.encode('utf-8')) def test_must_encode_utf8(self): from repoze.who._helpers import must_encode foo = b'b\xc3\xa2tard' self.assertTrue(must_encode(foo) is foo) def test_must_encode_latin1(self): from repoze.who._helpers import must_encode foo = b'b\xe2tard' self.assertTrue(must_encode(foo) is foo) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591215668.0 repoze.who-3.0.0/repoze/who/tests/test_api.py0000644000175000017500000014250400000000000022412 0ustar00tseavertseaver00000000000000import unittest class Test_get_api(unittest.TestCase): def _callFUT(self, environ): from repoze.who.api import get_api return get_api(environ) def test___call___empty_environ(self): environ = {} api = self._callFUT(environ) self.assertTrue(api is None) def test___call___w_api_in_environ(self): expected = object() environ = {'repoze.who.api': expected} api = self._callFUT(environ) self.assertTrue(api is expected) class APIFactoryTests(unittest.TestCase): def _getTargetClass(self): from repoze.who.api import APIFactory return APIFactory def _makeOne(self, plugins=None, identifiers=None, authenticators=None, challengers=None, mdproviders=None, request_classifier=None, challenge_decider=None, remote_user_key=None, logger=None, ): if plugins is None: plugins = {} if identifiers is None: identifiers = () if authenticators is None: authenticators = () if challengers is None: challengers = () if mdproviders is None: mdproviders = () return self._getTargetClass()(identifiers, authenticators, challengers, mdproviders, request_classifier, challenge_decider, remote_user_key, logger, ) def test_class_conforms_to_IAPIFactory(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAPIFactory verifyClass(IAPIFactory, self._getTargetClass()) def test_instance_conforms_to_IAPIFactory(self): from zope.interface.verify import verifyObject from repoze.who.interfaces import IAPIFactory verifyObject(IAPIFactory, self._makeOne()) def test_ctor_defaults(self): factory = self._makeOne() self.assertEqual(len(factory.identifiers), 0) self.assertEqual(len(factory.authenticators), 0) self.assertEqual(len(factory.challengers), 0) self.assertEqual(len(factory.mdproviders), 0) self.assertEqual(factory.request_classifier, None) self.assertEqual(factory.challenge_decider, None) self.assertEqual(factory.logger, None) def test___call___empty_environ(self): from repoze.who.api import API environ = {} factory = self._makeOne() api = factory(environ) self.assertTrue(isinstance(api, API)) self.assertTrue(environ['repoze.who.api'] is api) def test___call___w_api_in_environ(self): expected = object() environ = {'repoze.who.api': expected} factory = self._makeOne() api = factory(environ) self.assertTrue(api is expected) class TestMakeRegistries(unittest.TestCase): def _callFUT(self, identifiers, authenticators, challengers, mdproviders): from repoze.who.api import make_registries return make_registries(identifiers, authenticators, challengers, mdproviders) def test_empty(self): iface_reg, name_reg = self._callFUT([], [], [], []) self.assertEqual(iface_reg, {}) self.assertEqual(name_reg, {}) def test_brokenimpl(self): from zope.interface.exceptions import Invalid expected_exc = (Invalid, ValueError) # BBB for zope.interface < 5.0.0 with self.assertRaises(expected_exc): self._callFUT([(None, object())], [], [], []) def test_ok(self): from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IMetadataProvider credentials1 = {'login':'chris', 'password':'password'} dummy_id1 = DummyIdentifier(credentials1) credentials2 = {'login':'chris', 'password':'password'} dummy_id2 = DummyIdentifier(credentials2) identifiers = [ ('id1', dummy_id1), ('id2', dummy_id2) ] dummy_auth = DummyAuthenticator(None) authenticators = [ ('auth', dummy_auth) ] dummy_challenger = DummyChallenger(None) challengers = [ ('challenger', dummy_challenger) ] dummy_mdprovider = DummyMDProvider() mdproviders = [ ('mdprovider', dummy_mdprovider) ] iface_reg, name_reg = self._callFUT(identifiers, authenticators, challengers, mdproviders) self.assertEqual(iface_reg[IIdentifier], [dummy_id1, dummy_id2]) self.assertEqual(iface_reg[IAuthenticator], [dummy_auth]) self.assertEqual(iface_reg[IChallenger], [dummy_challenger]) self.assertEqual(iface_reg[IMetadataProvider], [dummy_mdprovider]) self.assertEqual(name_reg['id1'], dummy_id1) self.assertEqual(name_reg['id2'], dummy_id2) self.assertEqual(name_reg['auth'], dummy_auth) self.assertEqual(name_reg['challenger'], dummy_challenger) self.assertEqual(name_reg['mdprovider'], dummy_mdprovider) class TestMatchClassification(unittest.TestCase): def _getFUT(self): from repoze.who.api import match_classification return match_classification def test_match_classification(self): f = self._getFUT() from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IAuthenticator multi1 = DummyMultiPlugin() multi2 = DummyMultiPlugin() multi1.classifications = {IIdentifier:('foo', 'bar'), IChallenger:('buz',), IAuthenticator:None} multi2.classifications = {IIdentifier:('foo', 'baz', 'biz')} plugins = (multi1, multi2) # specific self.assertEqual(f(IIdentifier, plugins, 'foo'), [multi1, multi2]) self.assertEqual(f(IIdentifier, plugins, 'bar'), [multi1]) self.assertEqual(f(IIdentifier, plugins, 'biz'), [multi2]) # any for multi2 self.assertEqual(f(IChallenger, plugins, 'buz'), [multi1, multi2]) # any for either self.assertEqual(f(IAuthenticator, plugins, 'buz'), [multi1, multi2]) class APITests(unittest.TestCase): def _getTargetClass(self): from repoze.who.api import API return API def _makeOne(self, environ=None, identifiers=None, authenticators=None, challengers=None, request_classifier=None, mdproviders=None, challenge_decider=None, remote_user_key=None, logger=None ): if environ is None: environ = {} if identifiers is None: identifiers = [] if authenticators is None: authenticators = [] if challengers is None: challengers = [] if request_classifier is None: request_classifier = DummyRequestClassifier() if mdproviders is None: mdproviders = [] if challenge_decider is None: challenge_decider = DummyChallengeDecider() api = self._getTargetClass()(environ, identifiers, authenticators, challengers, mdproviders, request_classifier, challenge_decider, remote_user_key, logger, ) return api def _makeEnviron(self): from wsgiref.util import setup_testing_defaults environ = {} setup_testing_defaults(environ) return environ def test_class_conforms_to_IAPI(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAPI verifyClass(IAPI, self._getTargetClass()) def test_ctor_accepts_logger_instance(self): logger = DummyLogger() api = self._makeOne(logger=logger) self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 0) def test_authenticate_no_identities(self): logger = DummyLogger() environ = self._makeEnviron() plugin = DummyNoResultsIdentifier() plugins = [ ('dummy', plugin) ] api = self._makeOne(environ=environ, identifiers=plugins, logger=logger) identity = api.authenticate() self.assertEqual(identity, None) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(logger._info[1], 'no identities found, ' 'not authenticating') def test_authenticate_w_identities_no_authenticators(self): logger = DummyLogger() environ = self._makeEnviron() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('i', identifier) ] api = self._makeOne(environ=environ, identifiers=identifiers, logger=logger) identity = api.authenticate() self.assertEqual(identity, None) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: browser') # Hmm, should this message distinguish "none found" from # "none authenticated"? self.assertEqual(logger._info[1], 'no identities found, ' 'not authenticating') #def test_authenticate_w_identities_w_authenticators_miss(self): def test_authenticate_w_identities_w_authenticators_hit(self): logger = DummyLogger() environ = self._makeEnviron() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('i', identifier) ] authenticator = DummyAuthenticator('chrisid') authenticators = [ ('a', authenticator) ] api = self._makeOne(environ=environ, identifiers=identifiers, authenticators=authenticators, logger=logger) identity = api.authenticate() self.assertEqual(identity['repoze.who.userid'], 'chrisid') self.assertTrue(identity['identifier'] is identifier) self.assertTrue(identity['authenticator'] is authenticator) self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') def test_challenge_noidentifier_noapp(self): logger = DummyLogger() identity = {'login':'chris', 'password':'password'} environ = self._makeEnviron() environ['repoze.who.identity'] = identity challenger = DummyChallenger() plugins = [ ('challenge', challenger) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match', logger=logger, ) app = api.challenge('401 Unauthorized', []) self.assertEqual(app, None) self.assertEqual(environ['challenged'], None) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: match') self.assertEqual(logger._info[1], 'no challenge app returned') self.assertEqual(len(logger._debug), 2) self.assertTrue(logger._debug[0].startswith( 'challengers registered: [')) self.assertTrue(logger._debug[1].startswith( 'challengers matched for ' 'classification "match": [')) def test_challenge_noidentifier_with_app(self): logger = DummyLogger() identity = {'login':'chris', 'password':'password'} environ = self._makeEnviron() environ['repoze.who.identity'] = identity app = DummyApp() challenger = DummyChallenger(app) plugins = [ ('challenge', challenger) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match', logger=logger, ) result = api.challenge('401 Unauthorized', []) self.assertEqual(result, app) self.assertEqual(environ['challenged'], app) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: match') self.assertTrue(logger._info[1].startswith('challenger plugin ')) self.assertTrue(logger._info[1].endswith( '"challenge" returned an app')) self.assertEqual(len(logger._debug), 2) self.assertTrue(logger._debug[0].startswith( 'challengers registered: [')) self.assertTrue(logger._debug[1].startswith( 'challengers matched for ' 'classification "match": [')) def test_challenge_identifier_no_app_no_forget_headers(self): logger = DummyLogger() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identity = {'login':'chris', 'password':'password', 'identifier': identifier} environ = self._makeEnviron() environ['repoze.who.identity'] = identity challenger = DummyChallenger() plugins = [ ('challenge', challenger) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match', logger=logger, ) result = api.challenge('401 Unauthorized', []) self.assertEqual(result, None) self.assertEqual(environ['challenged'], None) self.assertEqual(identifier.forgotten, identity) self.assertEqual(len(logger._info), 3) self.assertEqual(logger._info[0], 'request classification: match') self.assertTrue(logger._info[1].startswith('forgetting via headers ')) self.assertEqual(logger._info[2], 'no challenge app returned') self.assertEqual(len(logger._debug), 2) self.assertTrue(logger._debug[0].startswith( 'challengers registered: [')) self.assertTrue(logger._debug[1].startswith( 'challengers matched for ' 'classification "match": [')) def test_challenge_identifier_app_no_forget_headers(self): logger = DummyLogger() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identity = {'login':'chris', 'password':'password', 'identifier': identifier} environ = self._makeEnviron() environ['repoze.who.identity'] = identity app = DummyApp() challenger = DummyChallenger(app) plugins = [ ('challenge', challenger) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match', logger=logger, ) result = api.challenge('401 Unauthorized', []) self.assertEqual(result, app) self.assertEqual(environ['challenged'], app) self.assertEqual(identifier.forgotten, identity) self.assertEqual(len(logger._info), 3) self.assertEqual(logger._info[0], 'request classification: match') self.assertTrue(logger._info[1].startswith('forgetting via headers ')) self.assertTrue(logger._info[2].startswith('challenger plugin ')) self.assertTrue(logger._info[2].endswith( '"challenge" returned an app')) self.assertEqual(len(logger._debug), 2) self.assertTrue(logger._debug[0].startswith( 'challengers registered: [')) self.assertTrue(logger._debug[1].startswith( 'challengers matched for ' 'classification "match": [')) def test_challenge_identifier_no_app_forget_headers(self): FORGET_HEADERS = [('X-testing-forget', 'Oubliez!')] logger = DummyLogger() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials, forget_headers=FORGET_HEADERS) identity = {'login':'chris', 'password':'password', 'identifier': identifier} environ = self._makeEnviron() environ['repoze.who.identity'] = identity app = DummyApp() challenger = DummyChallenger(app) plugins = [ ('challenge', challenger) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match', logger=logger, ) result = api.challenge('401 Unauthorized', []) self.assertEqual(result, app) self.assertEqual(environ['challenged'], app) self.assertEqual(challenger._challenged_with[3], FORGET_HEADERS) self.assertEqual(len(logger._info), 3) self.assertEqual(logger._info[0], 'request classification: match') self.assertTrue(logger._info[1].startswith( 'forgetting via headers from')) self.assertTrue(logger._info[1].endswith(repr(FORGET_HEADERS))) self.assertTrue(logger._info[2].startswith('challenger plugin ')) self.assertTrue(logger._info[2].endswith( '"challenge" returned an app')) self.assertEqual(len(logger._debug), 2) self.assertTrue(logger._debug[0].startswith( 'challengers registered: [')) self.assertTrue(logger._debug[1].startswith( 'challengers matched for ' 'classification "match": [')) def test_multi_challenge_firstwins(self): credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identity = {'login':'chris', 'password':'password', 'identifier': identifier} environ = self._makeEnviron() environ['repoze.who.identity'] = identity app1 = DummyApp() app2 = DummyApp() challenger1 = DummyChallenger(app1) challenger2 = DummyChallenger(app2) plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match') result = api.challenge('401 Unauthorized', []) self.assertEqual(result, app1) self.assertEqual(environ['challenged'], app1) self.assertEqual(identifier.forgotten, identity) def test_multi_challenge_skipnomatch_findimplicit(self): from repoze.who.interfaces import IChallenger credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identity = {'login':'chris', 'password':'password', 'identifier': identifier} environ = self._makeEnviron() environ['repoze.who.identity'] = identity app1 = DummyApp() app2 = DummyApp() challenger1 = DummyChallenger(app1) challenger1.classifications = {IChallenger:['nomatch']} challenger2 = DummyChallenger(app2) challenger2.classifications = {IChallenger:None} plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match') result = api.challenge('401 Unauthorized', []) self.assertEqual(result, app2) self.assertEqual(environ['challenged'], app2) self.assertEqual(identifier.forgotten, identity) def test_multi_challenge_skipnomatch_findexplicit(self): from repoze.who.interfaces import IChallenger credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identity = {'login':'chris', 'password':'password', 'identifier': identifier} environ = self._makeEnviron() environ['repoze.who.identity'] = identity app1 = DummyApp() app2 = DummyApp() challenger1 = DummyChallenger(app1) challenger1.classifications = {IChallenger:['nomatch']} challenger2 = DummyChallenger(app2) challenger2.classifications = {IChallenger:['match']} plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] api = self._makeOne(environ=environ, challengers=plugins, request_classifier=lambda environ: 'match') result = api.challenge('401 Unauthorized', []) self.assertEqual(result, app2) self.assertEqual(environ['challenged'], app2) self.assertEqual(identifier.forgotten, identity) def test_remember_identifier_plugin_returns_none(self): identity = {'identifier': DummyNoResultsIdentifier()} api = self._makeOne() headers = api.remember(identity=identity) self.assertEqual(tuple(headers), ()) def test_remember_no_identity_passed_or_in_environ(self): logger = DummyLogger() environ = self._makeEnviron() api = self._makeOne(environ=environ) self.assertEqual(len(api.remember()), 0) self.assertEqual(len(logger._info), 0) self.assertEqual(len(logger._debug), 0) def test_remember_no_identity_passed_but_in_environ(self): HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] logger = DummyLogger() environ = self._makeEnviron() environ['repoze.who.identity'] = { 'identifier': DummyIdentifier(remember_headers=HEADERS)} api = self._makeOne(environ=environ, logger=logger) self.assertEqual(api.remember(), HEADERS) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: browser') self.assertTrue(logger._info[1].startswith( 'remembering via headers from')) self.assertTrue(logger._info[1].endswith(repr(HEADERS))) self.assertEqual(len(logger._debug), 0) def test_remember_w_identity_passed_no_identifier(self): logger = DummyLogger() environ = self._makeEnviron() api = self._makeOne(environ=environ, logger=logger) identity = {} self.assertEqual(len(api.remember(identity)), 0) self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 0) def test_remember_w_identity_passed_w_identifier(self): HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] logger = DummyLogger() environ = self._makeEnviron() api = self._makeOne(environ=environ, logger=logger) identity = {'identifier': DummyIdentifier(remember_headers=HEADERS)} self.assertEqual(api.remember(identity), HEADERS) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: browser') self.assertTrue(logger._info[1].startswith( 'remembering via headers from')) self.assertTrue(logger._info[1].endswith(repr(HEADERS))) self.assertEqual(len(logger._debug), 0) def test_forget_identifier_plugin_returns_none(self): identity = {'identifier': DummyNoResultsIdentifier()} api = self._makeOne() headers = api.forget(identity=identity) self.assertEqual(tuple(headers), ()) def test_forget_no_identity_passed_or_in_environ(self): logger = DummyLogger() environ = self._makeEnviron() api = self._makeOne(environ=environ, logger=logger) self.assertEqual(len(api.forget()), 0) self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 0) def test_forget_no_identity_passed_but_in_environ(self): HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] logger = DummyLogger() environ = self._makeEnviron() environ['repoze.who.identity'] = { 'identifier': DummyIdentifier(forget_headers=HEADERS)} api = self._makeOne(environ=environ, logger=logger) self.assertEqual(api.forget(), HEADERS) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: browser') self.assertTrue(logger._info[1].startswith( 'forgetting via headers from')) self.assertTrue(logger._info[1].endswith(repr(HEADERS))) self.assertEqual(len(logger._debug), 0) def test_forget_w_identity_passed_no_identifier(self): environ = self._makeEnviron() logger = DummyLogger() api = self._makeOne(environ=environ, logger=logger) identity = {} self.assertEqual(len(api.forget(identity)), 0) self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 0) def test_forget_w_identity_passed_w_identifier(self): HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] logger = DummyLogger() environ = self._makeEnviron() api = self._makeOne(environ=environ, logger=logger) identity = {'identifier': DummyIdentifier(forget_headers=HEADERS)} self.assertEqual(api.forget(identity), HEADERS) self.assertEqual(len(logger._info), 2) self.assertEqual(logger._info[0], 'request classification: browser') self.assertTrue(logger._info[1].startswith( 'forgetting via headers from')) self.assertTrue(logger._info[1].endswith(repr(HEADERS))) self.assertEqual(len(logger._debug), 0) def test_login_w_identifier_name_hit(self): REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] FORGET_HEADERS = [('Spam', 'Blah')] authenticator = DummyAuthenticator('chrisid') environ = self._makeEnviron() identifiers = [('bogus', DummyNoResultsIdentifier()), ('valid', DummyIdentifier( remember_headers=REMEMBER_HEADERS)), ] api = self._makeOne(identifiers=identifiers, authenticators=[('authentic', authenticator)], environ=environ) identity, headers = api.login({'login': 'chrisid'}, 'valid') self.assertEqual(identity['repoze.who.userid'], 'chrisid') self.assertEqual(headers, REMEMBER_HEADERS) def test_login_wo_identifier_name_hit(self): REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] FORGET_HEADERS = [('Spam', 'Blah')] authenticator = DummyAuthenticator('chrisid') environ = self._makeEnviron() identifiers = [('bogus', DummyIdentifier( remember_headers=REMEMBER_HEADERS[:1])), ('valid', DummyIdentifier( remember_headers=REMEMBER_HEADERS[1:])), ] api = self._makeOne(identifiers=identifiers, authenticators=[('authentic', authenticator)], environ=environ) identity, headers = api.login({'login': 'chrisid'}) self.assertEqual(identity['repoze.who.userid'], 'chrisid') self.assertEqual(headers, REMEMBER_HEADERS) def test_login_w_identifier_name_miss(self): REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] FORGET_HEADERS = [('Spam', 'Blah')] authenticator = DummyFailAuthenticator() environ = self._makeEnviron() identifiers = [('bogus', DummyNoResultsIdentifier()), ('valid', DummyIdentifier( remember_headers=REMEMBER_HEADERS, forget_headers=FORGET_HEADERS)), ] api = self._makeOne(identifiers=identifiers, authenticators=[('authentic', authenticator)], environ=environ) identity, headers = api.login({'login': 'notchrisid'}, 'valid') self.assertEqual(identity, None) self.assertEqual(headers, FORGET_HEADERS) def test_logout_wo_identifier_name_miss(self): FORGET_HEADERS = [('Spam', 'Blah')] environ = self._makeEnviron() identifiers = [('valid', DummyIdentifier( forget_headers=FORGET_HEADERS[:1])), ('bogus', DummyIdentifier( forget_headers=FORGET_HEADERS[1:])), ] api = self._makeOne(identifiers=identifiers, environ=environ) headers = api.logout() self.assertEqual(headers, FORGET_HEADERS) def test_logout_w_identifier_name(self): FORGET_HEADERS = [('Spam', 'Blah')] environ = self._makeEnviron() identifiers = [('bogus', DummyNoResultsIdentifier()), ('valid', DummyIdentifier( forget_headers=FORGET_HEADERS)), ] api = self._makeOne(identifiers=identifiers, environ=environ) headers = api.logout('valid') self.assertEqual(headers, FORGET_HEADERS) def test_logout_wo_identifier_name(self): REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] FORGET_HEADERS = [('Spam', 'Blah')] authenticator = DummyFailAuthenticator() environ = self._makeEnviron() identifiers = [('bogus', DummyNoResultsIdentifier()), ('valid', DummyIdentifier( forget_headers=FORGET_HEADERS)), ] api = self._makeOne(identifiers=identifiers, authenticators=[('authentic', authenticator)], environ=environ) headers = api.logout() self.assertEqual(headers, FORGET_HEADERS) def test_logout_removes_repoze_who_identity(self): authenticator = DummyFailAuthenticator() environ = self._makeEnviron() environ['repoze.who.identity'] = 'identity' identifiers = [('valid', DummyNoResultsIdentifier())] api = self._makeOne(identifiers=identifiers, authenticators=[('authentic', authenticator)], environ=environ) api.logout() self.assertFalse('repoze.who.identity' in environ) def test__identify_success(self): environ = self._makeEnviron() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('i', identifier) ] api = self._makeOne(environ=environ, identifiers=identifiers) results = api._identify() self.assertEqual(len(results), 1) new_identifier, identity = results[0] self.assertEqual(new_identifier, identifier) self.assertEqual(identity['login'], 'chris') self.assertEqual(identity['password'], 'password') def test__identify_success_empty_identity(self): environ = self._makeEnviron() identifier = DummyIdentifier({}) identifiers = [ ('i', identifier) ] api = self._makeOne(environ=environ, identifiers=identifiers) results = api._identify() self.assertEqual(len(results), 1) new_identifier, identity = results[0] self.assertEqual(new_identifier, identifier) self.assertEqual(identity, {}) def test__identify_fail(self): logger = DummyLogger() environ = self._makeEnviron() plugin = DummyNoResultsIdentifier() plugins = [ ('dummy', plugin) ] api = self._makeOne(environ=environ, identifiers=plugins, logger=logger) results = api._identify() self.assertEqual(len(results), 0) self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 4) self.assertTrue(logger._debug[0].startswith( 'identifier plugins registered: [')) self.assertTrue(logger._debug[1].startswith( 'identifier plugins matched for ' 'classification "browser": [')) self.assertTrue(logger._debug[2].startswith( 'no identity returned from <')) self.assertTrue(logger._debug[2].endswith('> (None)')) self.assertEqual(logger._debug[3], 'identities found: []') def test__identify_success_skip_noresults(self): environ = self._makeEnviron() api = self._makeOne() plugin1 = DummyNoResultsIdentifier() credentials = {'login':'chris', 'password':'password'} plugin2 = DummyIdentifier(credentials) plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] api = self._makeOne(environ=environ, identifiers=plugins) results = api._identify() self.assertEqual(len(results), 1) new_identifier, identity = results[0] self.assertEqual(new_identifier, plugin2) self.assertEqual(identity['login'], 'chris') self.assertEqual(identity['password'], 'password') def test__identify_success_multiresults(self): environ = self._makeEnviron() api = self._makeOne() plugin1 = DummyIdentifier({'login':'fred','password':'fred'}) plugin2 = DummyIdentifier({'login':'bob','password':'bob'}) plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] api = self._makeOne(environ=environ, identifiers=plugins) results = api._identify() self.assertEqual(len(results), 2) new_identifier, identity = results[0] self.assertEqual(new_identifier, plugin1) self.assertEqual(identity['login'], 'fred') self.assertEqual(identity['password'], 'fred') new_identifier, identity = results[1] self.assertEqual(new_identifier, plugin2) self.assertEqual(identity['login'], 'bob') self.assertEqual(identity['password'], 'bob') def test__identify_find_implicit_classifier(self): environ = self._makeEnviron() api = self._makeOne() plugin1 = DummyIdentifier({'login':'fred','password':'fred'}) from repoze.who.interfaces import IIdentifier plugin1.classifications = {IIdentifier:['nomatch']} plugin2 = DummyIdentifier({'login':'bob','password':'bob'}) plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] api = self._makeOne(environ=environ, identifiers=plugins, request_classifier=lambda environ: 'match') results = api._identify() self.assertEqual(len(results), 1) plugin, creds = results[0] self.assertEqual(creds['login'], 'bob') self.assertEqual(creds['password'], 'bob') self.assertEqual(plugin, plugin2) def test__identify_find_explicit_classifier(self): environ = self._makeEnviron() from repoze.who.interfaces import IIdentifier plugin1 = DummyIdentifier({'login':'fred','password':'fred'}) plugin1.classifications = {IIdentifier:['nomatch']} plugin2 = DummyIdentifier({'login':'bob','password':'bob'}) plugin2.classifications = {IIdentifier:['match']} plugins= [ ('identifier1', plugin1), ('identifier2', plugin2) ] api = self._makeOne(environ=environ, identifiers=plugins, request_classifier=lambda environ: 'match') results = api._identify() self.assertEqual(len(results), 1) plugin, creds = results[0] self.assertEqual(creds['login'], 'bob') self.assertEqual(creds['password'], 'bob') self.assertEqual(plugin, plugin2) def test__authenticate_success(self): environ = self._makeEnviron() plugin1 = DummyAuthenticator('a') plugins = [ ('identifier1', plugin1) ] api = self._makeOne(environ=environ, authenticators=plugins) identities = [ (None, {'login':'chris', 'password':'password'}) ] results = api._authenticate(identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0)) self.assertEqual(authenticator, plugin1) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'a') def test__authenticate_fail(self): logger = DummyLogger() environ = self._makeEnviron() # no authenticators api = self._makeOne(environ=environ, logger=logger) identities = [ (None, {'login':'chris', 'password':'password'}) ] result = api._authenticate(identities) self.assertEqual(len(result), 0) self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 3) self.assertEqual(logger._debug[0], 'authenticator plugins ' 'registered: []') self.assertEqual(logger._debug[1], 'authenticator plugins matched ' 'for classification "browser": []') self.assertEqual(logger._debug[2], 'identities authenticated: []') def test__authenticate_success_skip_fail(self): logger = DummyLogger() environ = self._makeEnviron() plugin1 = DummyFailAuthenticator() plugin2 = DummyAuthenticator() plugins = [ ('dummy1', plugin1), ('dummy2', plugin2) ] api = self._makeOne(authenticators=plugins, logger=logger) creds = {'login':'chris', 'password':'password'} identities = [ (None, {'login':'chris', 'password':'password'}) ] results = api._authenticate(identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (1,0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris') self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 5) self.assertTrue(logger._debug[0].startswith( 'authenticator plugins registered: [')) self.assertTrue(logger._debug[1].startswith( 'authenticator plugins matched for ' 'classification "browser": [')) self.assertTrue(logger._debug[2].startswith('no userid returned from')) self.assertTrue(logger._debug[3].startswith('userid returned from')) self.assertTrue(logger._debug[3].endswith('"chris"')) self.assertTrue(logger._debug[4].startswith( 'identities authenticated: [((1, 0),')) def test__authenticate_success_multiresult(self): logger = DummyLogger() environ = self._makeEnviron() plugin1 = DummyAuthenticator('chris_id1') plugin2 = DummyAuthenticator('chris_id2') plugins = [ ('dummy1',plugin1), ('dummy2',plugin2) ] api = self._makeOne(environ=environ, authenticators=plugins, logger=logger) creds = {'login':'chris', 'password':'password'} identities = [ (None, {'login':'chris', 'password':'password'}) ] results = api._authenticate(identities) self.assertEqual(len(results), 2) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0,)) self.assertEqual(authenticator, plugin1) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id1') result = results[1] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (1,0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id2') self.assertEqual(len(logger._info), 1) self.assertEqual(logger._info[0], 'request classification: browser') self.assertEqual(len(logger._debug), 5) self.assertTrue(logger._debug[0].startswith( 'authenticator plugins registered: [')) self.assertTrue(logger._debug[1].startswith( 'authenticator plugins matched for ' 'classification "browser": [')) self.assertTrue(logger._debug[2].startswith('userid returned from')) self.assertTrue(logger._debug[2].endswith('"chris_id1"')) self.assertTrue(logger._debug[3].startswith('userid returned from')) self.assertTrue(logger._debug[3].endswith('"chris_id2"')) self.assertTrue(logger._debug[4].startswith( 'identities authenticated: [((0, 0),') ) def test__authenticate_find_implicit_classifier(self): from repoze.who.interfaces import IAuthenticator environ = self._makeEnviron() plugin1 = DummyAuthenticator('chris_id1') plugin1.classifications = {IAuthenticator:['nomatch']} plugin2 = DummyAuthenticator('chris_id2') plugins = [ ('auth1', plugin1), ('auth2', plugin2) ] api = self._makeOne(environ=environ, authenticators=plugins, request_classifier=lambda environ: 'match') identities = [ (None, {'login':'chris', 'password':'password'}) ] results = api._authenticate(identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id2') def test__authenticate_find_explicit_classifier(self): from repoze.who.interfaces import IAuthenticator environ = self._makeEnviron() plugin1 = DummyAuthenticator('chris_id1') plugin1.classifications = {IAuthenticator:['nomatch']} plugin2 = DummyAuthenticator('chris_id2') plugin2.classifications = {IAuthenticator:['match']} plugins = [ ('auth1', plugin1), ('auth2', plugin2) ] api = self._makeOne(environ=environ, authenticators=plugins, request_classifier=lambda environ: 'match') identities = [ (None, {'login':'chris', 'password':'password'}) ] results = api._authenticate(identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0, 0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id2') def test__authenticate_user_null_but_not_none(self): environ = self._makeEnviron() plugin1 = DummyAuthenticator(0) plugins = [ ('identifier1', plugin1) ] api = self._makeOne(environ=environ, authenticators=plugins) identities = [ (None, {'login':'chris', 'password':'password'}) ] results = api._authenticate(identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0)) self.assertEqual(authenticator, plugin1) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 0) def test__add_metadata(self): environ = self._makeEnviron() plugin1 = DummyMDProvider({'foo':'bar'}) plugin2 = DummyMDProvider({'fuz':'baz'}) plugins = [ ('meta1', plugin1), ('meta2', plugin2) ] api = self._makeOne(environ=environ, mdproviders=plugins) classification = '' identity = {} results = api._add_metadata(identity) self.assertEqual(identity['foo'], 'bar') self.assertEqual(identity['fuz'], 'baz') def test__add_metadata_w_classification(self): environ = self._makeEnviron() plugin1 = DummyMDProvider({'foo':'bar'}) plugin2 = DummyMDProvider({'fuz':'baz'}) from repoze.who.interfaces import IMetadataProvider plugin2.classifications = {IMetadataProvider:['foo']} plugins = [ ('meta1', plugin1), ('meta2', plugin2) ] api = self._makeOne(environ=environ, mdproviders=plugins) classification = 'monkey' identity = {} api._add_metadata(identity) self.assertEqual(identity['foo'], 'bar') self.assertEqual(identity.get('fuz'), None) class TestIdentityDict(unittest.TestCase): def _getTargetClass(self): from repoze.who.api import Identity return Identity def _makeOne(self, **kw): klass = self._getTargetClass() return klass(**kw) def test_str(self): identity = self._makeOne(foo=1) self.assertTrue(str(identity).startswith('