From 32116997fb057e3c19edbd9a05022c72342a172f Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 3 Sep 2015 21:26:58 -0400 Subject: initial commit skeleton for module --- bonafide/.gitignore | 9 + bonafide/AUTHORS | 0 bonafide/CHANGELOG | 0 bonafide/LICENSE | 619 ++++++++++++++++++++++++ bonafide/README.rst | 6 + bonafide/pkg/__init__.py | 0 bonafide/pkg/requirements.pip | 3 + bonafide/pkg/utils.py | 84 ++++ bonafide/setup.cfg | 10 + bonafide/setup.py | 95 ++++ bonafide/src/leap/__init__.py | 6 + bonafide/src/leap/bonafide/__init__.py | 0 bonafide/src/leap/bonafide/bootstrap.py | 0 bonafide/src/leap/bonafide/cred_srp.py | 158 ++++++ bonafide/src/leap/bonafide/provider.py | 58 +++ bonafide/src/leap/bonafide/services/TODO | 1 + bonafide/src/leap/bonafide/services/__init__.py | 0 bonafide/src/leap/bonafide/services/eip.py | 0 bonafide/src/leap/bonafide/services/mail.py | 0 bonafide/src/leap/bonafide/services/soledad.py | 0 bonafide/src/leap/bonafide/session.py | 164 +++++++ bonafide/src/leap/bonafide/srp_auth.py | 120 +++++ bonafide/src/leap/bonafide/tests/__init__.py | 0 23 files changed, 1333 insertions(+) create mode 100644 bonafide/.gitignore create mode 100644 bonafide/AUTHORS create mode 100644 bonafide/CHANGELOG create mode 100644 bonafide/LICENSE create mode 100644 bonafide/README.rst create mode 100644 bonafide/pkg/__init__.py create mode 100644 bonafide/pkg/requirements.pip create mode 100644 bonafide/pkg/utils.py create mode 100644 bonafide/setup.cfg create mode 100644 bonafide/setup.py create mode 100644 bonafide/src/leap/__init__.py create mode 100644 bonafide/src/leap/bonafide/__init__.py create mode 100644 bonafide/src/leap/bonafide/bootstrap.py create mode 100644 bonafide/src/leap/bonafide/cred_srp.py create mode 100644 bonafide/src/leap/bonafide/provider.py create mode 100644 bonafide/src/leap/bonafide/services/TODO create mode 100644 bonafide/src/leap/bonafide/services/__init__.py create mode 100644 bonafide/src/leap/bonafide/services/eip.py create mode 100644 bonafide/src/leap/bonafide/services/mail.py create mode 100644 bonafide/src/leap/bonafide/services/soledad.py create mode 100644 bonafide/src/leap/bonafide/session.py create mode 100644 bonafide/src/leap/bonafide/srp_auth.py create mode 100644 bonafide/src/leap/bonafide/tests/__init__.py diff --git a/bonafide/.gitignore b/bonafide/.gitignore new file mode 100644 index 0000000..0e26c09 --- /dev/null +++ b/bonafide/.gitignore @@ -0,0 +1,9 @@ +*.pyc +*.egg +*.egg-info +*.swp +*.swo +dist/ +build/ +MANIFEST +_trial_temp diff --git a/bonafide/AUTHORS b/bonafide/AUTHORS new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/CHANGELOG b/bonafide/CHANGELOG new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/LICENSE b/bonafide/LICENSE new file mode 100644 index 0000000..bc08fe2 --- /dev/null +++ b/bonafide/LICENSE @@ -0,0 +1,619 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. diff --git a/bonafide/README.rst b/bonafide/README.rst new file mode 100644 index 0000000..510dc28 --- /dev/null +++ b/bonafide/README.rst @@ -0,0 +1,6 @@ +Bonafide +-------- +Secure user registration, authentication, and provider discovery for the LEAP +applications. + +https://leap.se/en/docs/design/bonafide diff --git a/bonafide/pkg/__init__.py b/bonafide/pkg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/pkg/requirements.pip b/bonafide/pkg/requirements.pip new file mode 100644 index 0000000..d9bbc3f --- /dev/null +++ b/bonafide/pkg/requirements.pip @@ -0,0 +1,3 @@ +srp +twisted>=14.0.0 +service-identity diff --git a/bonafide/pkg/utils.py b/bonafide/pkg/utils.py new file mode 100644 index 0000000..521cd4e --- /dev/null +++ b/bonafide/pkg/utils.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# utils.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Utils to help in the setup process +""" + +import os +import re +import sys + + +def get_reqs_from_files(reqfiles): + """ + Returns the contents of the top requirement file listed as a + string list with the lines + + @param reqfiles: requirement files to parse + @type reqfiles: list of str + """ + for reqfile in reqfiles: + if os.path.isfile(reqfile): + return open(reqfile, 'r').read().split('\n') + + +def parse_requirements(reqfiles=['requirements.txt', + 'requirements.pip', + 'pkg/requirements.pip']): + """ + Parses the requirement files provided. + + Checks the value of LEAP_VENV_SKIP_PYSIDE to see if it should + return PySide as a dep or not. Don't set, or set to 0 if you want + to install it through pip. + + @param reqfiles: requirement files to parse + @type reqfiles: list of str + """ + + requirements = [] + skip_pyside = os.getenv("LEAP_VENV_SKIP_PYSIDE", "0") != "0" + for line in get_reqs_from_files(reqfiles): + # -e git://foo.bar/baz/master#egg=foobar + if re.match(r'\s*-e\s+', line): + pass + # do not try to do anything with externals on vcs + # requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', + # line)) + # http://foo.bar/baz/foobar/zipball/master#egg=foobar + elif re.match(r'\s*https?:', line): + requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', + line)) + # -f lines are for index locations, and don't get used here + elif re.match(r'\s*-f\s+', line): + pass + + # argparse is part of the standard library starting with 2.7 + # adding it to the requirements list screws distro installs + elif line == 'argparse' and sys.version_info >= (2, 7): + pass + elif line == 'PySide' and skip_pyside: + pass + # do not include comments + elif line.lstrip().startswith('#'): + pass + else: + if line != '': + requirements.append(line) + + return requirements diff --git a/bonafide/setup.cfg b/bonafide/setup.cfg new file mode 100644 index 0000000..4a2ab2b --- /dev/null +++ b/bonafide/setup.cfg @@ -0,0 +1,10 @@ +[aliases] +test = trial + +[pep8] +exclude = versioneer.py,_version.py,*.egg,build,dist,docs +ignore = E731 + +[flake8] +exclude = versioneer.py,_version.py,*.egg,build,dist,docs +ignore = E731 diff --git a/bonafide/setup.py b/bonafide/setup.py new file mode 100644 index 0000000..eef1541 --- /dev/null +++ b/bonafide/setup.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# setup.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +setup file for leap.bonafide +""" +import re +from setuptools import setup, find_packages + +from pkg import utils + +# XXX TODO --- update to versioneer 0.15 --------------------- +#import versioneer +#versioneer.versionfile_source = 'src/leap/bonafide/_version.py' +#versioneer.versionfile_build = 'leap/common/_version.py' +#versioneer.tag_prefix = '' # tags are like 1.2.0 +#versioneer.parentdir_prefix = 'leap.bonafide-' +# -------------------------------------------------------------- + +parsed_reqs = utils.parse_requirements() + + +trove_classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + ("License :: OSI Approved :: GNU General " + "Public License v3 or later (GPLv3+)"), + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Topic :: Communications", + "Topic :: Security", + "Topic :: Utilities" +] + +DOWNLOAD_BASE = ('https://github.com/leapcode/bonafide/' + 'archive/%s.tar.gz') + +#_versions = versioneer.get_versions() +#VERSION = _versions['version'] +VERSION = '0.0.1' +#VERSION_FULL = _versions['full'] +DOWNLOAD_URL = "" + +# get the short version for the download url +#_version_short = re.findall('\d+\.\d+\.\d+', VERSION) +#if len(_version_short) > 0: + #VERSION_SHORT = _version_short[0] + #DOWNLOAD_URL = DOWNLOAD_BASE % VERSION_SHORT +# +#cmdclass = versioneer.get_cmdclass() +# ---------------------------------------------------------------- + +try: + long_description = open('README.rst').read() + '\n\n\n' + \ + open('CHANGELOG').read() +except Exception: + long_description = "" + +setup( + name='leap.bonafide', + version=VERSION, + #cmdclass=cmdclass, + url='https://leap.se/', + #download_url=DOWNLOAD_URL, + license='GPLv3+', + author='The LEAP Encryption Access Project', + author_email='info@leap.se', + maintainer='Kali Kaneko', + maintainer_email='kali@leap.se', + description='Common files used by the LEAP project.', + long_description=long_description, + classifiers=trove_classifiers, + namespace_packages=["leap"], + package_dir={'': 'src'}, + package_data={'': ['*.pem']}, + packages=find_packages('src'), + test_suite='leap.bonafide.tests', + install_requires=parsed_reqs, + include_package_data=True, + zip_safe=False, +) diff --git a/bonafide/src/leap/__init__.py b/bonafide/src/leap/__init__.py new file mode 100644 index 0000000..f48ad10 --- /dev/null +++ b/bonafide/src/leap/__init__.py @@ -0,0 +1,6 @@ +# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/bonafide/src/leap/bonafide/__init__.py b/bonafide/src/leap/bonafide/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/src/leap/bonafide/bootstrap.py b/bonafide/src/leap/bonafide/bootstrap.py new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/src/leap/bonafide/cred_srp.py b/bonafide/src/leap/bonafide/cred_srp.py new file mode 100644 index 0000000..5188830 --- /dev/null +++ b/bonafide/src/leap/bonafide/cred_srp.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# srp_cred.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Credential module for authenticating SRP requests against the LEAP platform. +""" + +# ----------------- DOC ------------------------------------------------------ +# See examples of cred modules: +# https://github.com/oubiwann-unsupported/txBrowserID/blob/master/browserid/checker.py +# http://stackoverflow.com/questions/19171686/book-twisted-network-programming-essentials-example-9-1-does-not-work +# ----------------- DOC ------------------------------------------------------ + +from zope.interface import implements, implementer, Interface, Attribute + +from twisted.cred import portal, credentials, error as credError +from twisted.cred.checkers import ICredentialsChecker +from twisted.internet import defer, reactor + +from leap.bonafide.session import LeapSession + + +@implementer(ICredentialsChecker) +class SRPCredentialsChecker(object): + + # TODO need to decide if the credentials that we pass here are per provider + # or not. + # I think it's better to have the credentials passed with the full user_id, + # and here split user/provider. + # XXX then we need to check if the provider is properly configured, to get + # the right api info AND the needed certificates. + # XXX might need to initialize credential checker with a ProviderAPI + + credentialInterfaces = (credentials.IUsernamePassword,) + + def requestAvatarId(self, credentials): + # TODO If we are already authenticated, we should just + # return the session object, somehow. + # XXX If not authenticated (ie, no cached credentials?) + # we pass credentials to srpauth.authenticate method + # another srpauth class should interface with the blocking srpauth + # library, and chain all the calls needed to do the handshake. + # Therefore: + # should keep reference to the srpauth instances somewhere + # TODO If we want to return an anonymous user (useful for getting the + # anon-vpn cert), we should return an empty tuple from here. + + return defer.maybeDeferred(_get_leap_session(credentials)).addCallback( + self._check_srp_auth) + + def _check_srp_auth(session, username): + if session.authenticated: + # is ok! --- should add it to some global cache? + return defer.succeed(username) + else: + return defer.fail(credError.UnauthorizedLogin( + "Bad username/password combination")) + + +def _get_leap_session(credentials): + session = LeapSession(credentials) + d = session.handshake() + d.addCallback(lambda _: session.authenticate()) + d.addCallback(lambda _: session) + return d + + +class ILeapUserAvatar(Interface): + + # TODO add attributes for username, uuid, token, session_id + + def logout(): + """ + Clean up per-login resource allocated to this avatar. + """ + + +@implementer(ILeapUserAvatar) +class LeapUserAvatar(object): + + # TODO initialize with: username, uuid, token, session_id + # TODO initialize provider data (for api) + # TODO how does this relate to LeapSession? maybe we should get one passed? + + def logout(self): + + # TODO reset the (global?) srpauth object. + # https://leap.se/en/docs/design/bonafide#logout + # DELETE API_BASE/logout(.json) + pass + + +class LeapAuthRealm(object): + """ + The realm corresponds to an application domain and is in charge of avatars, + which are network-accessible business logic objects. + """ + + # TODO should be initialized with provider API objects. + + implements(portal.IRealm) + + def requestAvatar(self, avatarId, mind, *interfaces): + + if ILeapUserAvatar in interfaces: + # XXX how should we get the details for the requested avatar? + avatar = LeapUserAvatar() + return ILeapUserAvatar, avatar, avatar.logout + + raise NotImplementedError( + "This realm only supports the ILeapUserAvatar interface.") + + +if __name__ == '__main__': + + # from the browser-id implementation + #import sys + #def _done(res): + #print res + #reactor.stop() + #assertion = sys.argv[1] + #d = defer.Deferred() + #reactor.callLater(0, d.callback, "http://localhost:8081") + #d.addCallback(SRPCredentialsChecker) + #d.addCallback(lambda c: c.requestAvatarId(assertion)) + #d.addBoth(_done) + #reactor.run() + + # XXX move boilerplate to some bitmask-core template. + + leap_realm = LeapAuthRealm() + # XXX should pass a provider mapping to realm too? + + leap_portal = portal.Portal(leap_realm) + + # XXX should we add an offline credentials checker, that's able + # to unlock local soledad sqlcipher backend? + # XXX should pass a provider mapping to credentials checker too? + srp_checker = SRPCredentialsChecker() + leap_portal.registerChecker(srp_checker) + + # XXX tie this to some sample server... + reactor.listenTCP(8000, EchoFactory(leap_portal)) + reactor.run() diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py new file mode 100644 index 0000000..ca2ea1d --- /dev/null +++ b/bonafide/src/leap/bonafide/provider.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# provier.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +LEAP Provider API. +""" + + +class LeapProviderApi(object): + # TODO when should the provider-api object be created? + + # XXX separate in auth-needing actions? + # XXX version this mapping !!! + + actions = { + 'signup': ('users', 'POST'), + 'handshake': ('sessions', 'POST'), + 'authenticate': ('sessions/{login}', 'PUT'), + 'update_user': ('users/{uid}', 'PUT'), + 'logout': ('logout', 'DELETE'), + 'get_vpn_cert': ('cert', 'POST'), + 'get_smtp_cert': ('smtp_cert', 'POST'), + } + + def __init__(self, uri, version): + self.uri = uri + self.version = version + + @property + def base_url(self): + return "https://{0}/{1}".format(self.uri, self.version) + + # XXX split in two different methods? + def get_uri_and_method(self, action_name, **extra_params): + action = self.actions.get(action_name, None) + if not action: + raise ValueError("Requested a non-existent action for this API") + resource, method = action + + uri = '{0}/{1}'.format(bytes(self.base_url), bytes(resource)).format( + **extra_params) + return uri, method + + # XXX add a provider_domain property, just to check if it's the right + # provider domain? diff --git a/bonafide/src/leap/bonafide/services/TODO b/bonafide/src/leap/bonafide/services/TODO new file mode 100644 index 0000000..97dc639 --- /dev/null +++ b/bonafide/src/leap/bonafide/services/TODO @@ -0,0 +1 @@ +# XXX this should be discoverable through the config. diff --git a/bonafide/src/leap/bonafide/services/__init__.py b/bonafide/src/leap/bonafide/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/src/leap/bonafide/services/eip.py b/bonafide/src/leap/bonafide/services/eip.py new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/src/leap/bonafide/services/mail.py b/bonafide/src/leap/bonafide/services/mail.py new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/src/leap/bonafide/services/soledad.py b/bonafide/src/leap/bonafide/services/soledad.py new file mode 100644 index 0000000..e69de29 diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py new file mode 100644 index 0000000..a113d0a --- /dev/null +++ b/bonafide/src/leap/bonafide/session.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# session.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +LEAP Session management. +""" +import cookielib +import urllib + +from twisted.internet import defer, reactor, protocol +from twisted.internet.ssl import Certificate +from twisted.web.client import Agent, CookieAgent, HTTPConnectionPool +from twisted.web.client import BrowserLikePolicyForHTTPS +from twisted.web.http_headers import Headers +from twisted.web.iweb import IBodyProducer +from twisted.python import log +from twisted.python.filepath import FilePath +from twisted.python import log +from zope.interface import implements + +from leap.bonafide import srp_auth + + +class LeapSession(object): + + def __init__(self, credentials, api, provider_cert): + # TODO check if an anonymous credentials is passed + # TODO -- we could decorate some methods so that they + # complain if we're not authenticated. + + self.username = credentials.username + self.password = credentials.password + + self._api = api + customPolicy = BrowserLikePolicyForHTTPS( + Certificate.loadPEM(FilePath(provider_cert).getContent())) + + # BUG XXX See https://twistedmatrix.com/trac/ticket/7843 + pool = HTTPConnectionPool(reactor, persistent=False) + agent = Agent(reactor, customPolicy, connectTimeout=30, pool=pool) + cookiejar = cookielib.CookieJar() + self._agent = CookieAgent(agent, cookiejar) + + self._srp_auth = srp_auth.SRPAuthMechanism() + self._srp_user = None + + @defer.inlineCallbacks + def authenticate(self): + srpuser, A = self._srp_auth.initialize( + self.username, self.password) + self._srp_user = srpuser + + uri, method = self._api.get_uri_and_method('handshake') + log.msg("%s to %s" % (method, uri)) + params = self._srp_auth.get_handshake_params(self.username, A) + handshake = yield httpRequest(self._agent, uri, values=params, + method=method) + + M = self._srp_auth.process_handshake(srpuser, handshake) + uri, method = self._api.get_uri_and_method( + 'authenticate', login=self.username) + log.msg("%s to %s" % (method, uri)) + params = self._srp_auth.get_authentication_params(M, A) + auth = yield httpRequest(self._agent, uri, values=params, + method=method) + + uuid, token, M2 = self._srp_auth.process_authentication(auth) + self._srp_auth.verify_authentication(srpuser, M2) + defer.succeed('ok') + # XXX get_session_id?? + # XXX return defer.succeed + + def is_authenticated(self): + if not self._srp_user: + return False + return self._srp_user.authenticated() + + +def httpRequest(agent, url, values={}, headers={}, method='POST'): + headers['Content-Type'] = ['application/x-www-form-urlencoded'] + data = urllib.urlencode(values) + d = agent.request(method, url, Headers(headers), + StringProducer(data) if data else None) + + def handle_response(response): + if response.code == 204: + d = defer.succeed('') + else: + class SimpleReceiver(protocol.Protocol): + def __init__(s, d): + s.buf = '' + s.d = d + + def dataReceived(s, data): + print "----> handle response: GOT DATA" + s.buf += data + + def connectionLost(s, reason): + print "CONNECTION LOST ---", reason + # TODO: test if reason is twisted.web.client.ResponseDone, + # if not, do an errback + s.d.callback(s.buf) + + d = defer.Deferred() + response.deliverBody(SimpleReceiver(d)) + return d + + d.addCallback(handle_response) + return d + + +class StringProducer(object): + + implements(IBodyProducer) + + def __init__(self, body): + self.body = body + self.length = len(body) + + def startProducing(self, consumer): + consumer.write(self.body) + return defer.succeed(None) + + def pauseProducing(self): + pass + + def stopProducing(self): + pass + +if __name__ == "__main__": + from leap.bonafide import provider + from twisted.cred.credentials import UsernamePassword + + api = provider.LeapProviderApi('api.cdev.bitmask.net:4430', 1) + credentials = UsernamePassword('test_deb_090', 'lalalala') + + cdev_pem = '/home/kali/.config/leap/providers/cdev.bitmask.net/keys/ca/cacert.pem' + session = LeapSession(credentials, api, cdev_pem) + + def print_result(result): + print "Auth OK" + print "result" + + def cbShutDown(ignored): + reactor.stop() + + d = session.authenticate() + d.addCallback(print_result) + d.addErrback(lambda f: log.err(f)) + d.addBoth(cbShutDown) + reactor.run() diff --git a/bonafide/src/leap/bonafide/srp_auth.py b/bonafide/src/leap/bonafide/srp_auth.py new file mode 100644 index 0000000..ac2cd67 --- /dev/null +++ b/bonafide/src/leap/bonafide/srp_auth.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# srp.py +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +SRP Authentication. +""" + +import binascii +import logging +import json + +import srp + +logger = logging.getLogger(__name__) + + +class SRPAuthMechanism(object): + + """ + Implement a protocol-agnostic SRP Authentication mechanism. + """ + + def initialize(self, username, password): + srp_user = srp.User(username.encode('utf-8'), + password.encode('utf-8'), + srp.SHA256, srp.NG_1024) + _, A = srp_user.start_authentication() + return srp_user, A + + def get_handshake_params(self, username, A): + return {'login': bytes(username), 'A': binascii.hexlify(A)} + + def process_handshake(self, srp_user, handshake_response): + challenge = json.loads(handshake_response) + self._check_for_errors(challenge) + salt = challenge.get('salt', None) + B = challenge.get('B', None) + unhex_salt, unhex_B = self._unhex_salt_B(salt, B) + M = srp_user.process_challenge(unhex_salt, unhex_B) + return M + + def get_authentication_params(self, M, A): + # I think A is not used in the server side + return {'client_auth': binascii.hexlify(M), 'A': binascii.hexlify(A)} + + def process_authentication(self, authentication_response): + auth = json.loads(authentication_response) + uuid = auth.get('id', None) + token = auth.get('token', None) + M2 = auth.get('M2', None) + self._check_auth_params(uuid, token, M2) + return uuid, token, M2 + + def verify_authentication(self, srp_user, M2): + unhex_M2 = _safe_unhexlify(M2) + srp_user.verify_session(unhex_M2) + assert srp_user.authenticated() + + def _check_for_errors(self, challenge): + if 'errors' in challenge: + msg = challenge['errors']['base'] + raise SRPAuthError(msg) + + def _unhex_salt_B(self, salt, B): + if salt is None: + raise SRPAuthNoSalt() + if B is None: + raise SRPAuthNoB() + try: + unhex_salt = _safe_unhexlify(salt) + unhex_B = _safe_unhexlify(B) + except (TypeError, ValueError) as e: + raise SRPAuthBadDataFromServer(str(e)) + return unhex_salt, unhex_B + + def _check_auth_params(self, uuid, token, M2): + if not all((uuid, token, M2)): + msg = '%r' % (M2, uuid, token,) + raise SRPAuthBadDataFromServer(msg) + + #XXX move to session ----------------------- + def get_session_id(self, cookies): + return cookies.get('_session_id', None) + #XXX move to session ----------------------- + + +def _safe_unhexlify(val): + return binascii.unhexlify(val) \ + if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) + + +class SRPAuthError(Exception): + """ + Base exception for srp authentication errors + """ + + +class SRPAuthNoSalt(SRPAuthError): + message = 'The server didn\'t send the salt parameter' + + +class SRPAuthNoB(SRPAuthError): + message = 'The server didn\'t send the B parameter' + +class SRPAuthBadDataFromServer(SRPAuthError): + pass diff --git a/bonafide/src/leap/bonafide/tests/__init__.py b/bonafide/src/leap/bonafide/tests/__init__.py new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3 From 47a1c704321f30c09007f7c60162c6c12c146b93 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 4 Sep 2015 00:38:59 -0400 Subject: factor out http utils, add decorator for authenticated methods --- bonafide/src/leap/bonafide/_decorators.py | 33 +++++++++ bonafide/src/leap/bonafide/_http.py | 90 ++++++++++++++++++++++++ bonafide/src/leap/bonafide/session.py | 113 +++++++++--------------------- bonafide/src/leap/bonafide/srp_auth.py | 23 +++--- 4 files changed, 164 insertions(+), 95 deletions(-) create mode 100644 bonafide/src/leap/bonafide/_decorators.py create mode 100644 bonafide/src/leap/bonafide/_http.py diff --git a/bonafide/src/leap/bonafide/_decorators.py b/bonafide/src/leap/bonafide/_decorators.py new file mode 100644 index 0000000..cfd6ec0 --- /dev/null +++ b/bonafide/src/leap/bonafide/_decorators.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# _decorators.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Decorators used in bonafide. +""" + + +def needs_authentication(func): + """ + Decorate a method so that it will not be called if the instance + attribute `is_authenticated` does not evaluate to True. + """ + def decorated(*args, **kwargs): + instance = args[0] + allowed = getattr(instance, 'is_authenticated') + if not allowed: + raise RuntimeError('This method requires authentication') + return func(*args, **kwargs) + return decorated diff --git a/bonafide/src/leap/bonafide/_http.py b/bonafide/src/leap/bonafide/_http.py new file mode 100644 index 0000000..6510e84 --- /dev/null +++ b/bonafide/src/leap/bonafide/_http.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# _http.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +twisted.web utils for bonafide. +""" +import cookielib +import urllib + +from twisted.internet import defer, protocol, reactor +from twisted.internet.ssl import Certificate +from twisted.python.filepath import FilePath +from twisted.web.client import Agent, CookieAgent +from twisted.web.client import BrowserLikePolicyForHTTPS +from twisted.web.http_headers import Headers +from twisted.web.iweb import IBodyProducer +from zope.interface import implements + + +def cookieAgentFactory(verify_path, connectTimeout=30): + customPolicy = BrowserLikePolicyForHTTPS( + Certificate.loadPEM(FilePath(verify_path).getContent())) + agent = Agent(reactor, customPolicy, connectTimeout=connectTimeout) + cookiejar = cookielib.CookieJar() + return CookieAgent(agent, cookiejar) + + +def httpRequest(agent, url, values={}, headers={}, method='POST'): + data = '' + if values: + data = urllib.urlencode(values) + headers['Content-Type'] = ['application/x-www-form-urlencoded'] + + def handle_response(response): + if response.code == 204: + d = defer.succeed('') + else: + class SimpleReceiver(protocol.Protocol): + def __init__(s, d): + s.buf = '' + s.d = d + + def dataReceived(s, data): + s.buf += data + + def connectionLost(s, reason): + # TODO: test if reason is twisted.web.client.ResponseDone, + # if not, do an errback + s.d.callback(s.buf) + d = defer.Deferred() + response.deliverBody(SimpleReceiver(d)) + return d + + d = agent.request(method, url, Headers(headers), + StringProducer(data) if data else None) + d.addCallback(handle_response) + return d + + +class StringProducer(object): + + implements(IBodyProducer) + + def __init__(self, body): + self.body = body + self.length = len(body) + + def startProducing(self, consumer): + consumer.write(self.body) + return defer.succeed(None) + + def pauseProducing(self): + pass + + def stopProducing(self): + pass diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index a113d0a..85e49e0 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -17,45 +17,35 @@ """ LEAP Session management. """ -import cookielib -import urllib - -from twisted.internet import defer, reactor, protocol -from twisted.internet.ssl import Certificate -from twisted.web.client import Agent, CookieAgent, HTTPConnectionPool -from twisted.web.client import BrowserLikePolicyForHTTPS -from twisted.web.http_headers import Headers -from twisted.web.iweb import IBodyProducer +from twisted.internet import defer, reactor from twisted.python import log -from twisted.python.filepath import FilePath -from twisted.python import log -from zope.interface import implements from leap.bonafide import srp_auth +from leap.bonafide._decorators import needs_authentication +from leap.bonafide._http import httpRequest, cookieAgentFactory class LeapSession(object): def __init__(self, credentials, api, provider_cert): - # TODO check if an anonymous credentials is passed - # TODO -- we could decorate some methods so that they - # complain if we're not authenticated. + # TODO check if an anonymous credentials is passed. + # TODO move provider_cert to api object. + # On creation, it should be able to retrieve all the info it needs + # (calling bootstrap). + # TODO could get a "provider" object instead. + # this provider can have an api attribute, + # and a "autoconfig" attribute passed on initialization. + # TODO get a file-descriptor for password if not in credentials self.username = credentials.username self.password = credentials.password - self._api = api - customPolicy = BrowserLikePolicyForHTTPS( - Certificate.loadPEM(FilePath(provider_cert).getContent())) - - # BUG XXX See https://twistedmatrix.com/trac/ticket/7843 - pool = HTTPConnectionPool(reactor, persistent=False) - agent = Agent(reactor, customPolicy, connectTimeout=30, pool=pool) - cookiejar = cookielib.CookieJar() - self._agent = CookieAgent(agent, cookiejar) + self._agent = cookieAgentFactory(provider_cert) self._srp_auth = srp_auth.SRPAuthMechanism() self._srp_user = None + self._token = None + self._uuid = None @defer.inlineCallbacks def authenticate(self): @@ -66,6 +56,7 @@ class LeapSession(object): uri, method = self._api.get_uri_and_method('handshake') log.msg("%s to %s" % (method, uri)) params = self._srp_auth.get_handshake_params(self.username, A) + handshake = yield httpRequest(self._agent, uri, values=params, method=method) @@ -74,72 +65,28 @@ class LeapSession(object): 'authenticate', login=self.username) log.msg("%s to %s" % (method, uri)) params = self._srp_auth.get_authentication_params(M, A) + auth = yield httpRequest(self._agent, uri, values=params, method=method) uuid, token, M2 = self._srp_auth.process_authentication(auth) self._srp_auth.verify_authentication(srpuser, M2) - defer.succeed('ok') - # XXX get_session_id?? - # XXX return defer.succeed + self._uuid = uuid + self._token = token + defer.returnValue('[OK] Credentias Authenticated through SRP') + + @needs_authentication + def logout(self): + print "Should logout..." + + @property def is_authenticated(self): if not self._srp_user: return False return self._srp_user.authenticated() -def httpRequest(agent, url, values={}, headers={}, method='POST'): - headers['Content-Type'] = ['application/x-www-form-urlencoded'] - data = urllib.urlencode(values) - d = agent.request(method, url, Headers(headers), - StringProducer(data) if data else None) - - def handle_response(response): - if response.code == 204: - d = defer.succeed('') - else: - class SimpleReceiver(protocol.Protocol): - def __init__(s, d): - s.buf = '' - s.d = d - - def dataReceived(s, data): - print "----> handle response: GOT DATA" - s.buf += data - - def connectionLost(s, reason): - print "CONNECTION LOST ---", reason - # TODO: test if reason is twisted.web.client.ResponseDone, - # if not, do an errback - s.d.callback(s.buf) - - d = defer.Deferred() - response.deliverBody(SimpleReceiver(d)) - return d - - d.addCallback(handle_response) - return d - - -class StringProducer(object): - - implements(IBodyProducer) - - def __init__(self, body): - self.body = body - self.length = len(body) - - def startProducing(self, consumer): - consumer.write(self.body) - return defer.succeed(None) - - def pauseProducing(self): - pass - - def stopProducing(self): - pass - if __name__ == "__main__": from leap.bonafide import provider from twisted.cred.credentials import UsernamePassword @@ -151,14 +98,18 @@ if __name__ == "__main__": session = LeapSession(credentials, api, cdev_pem) def print_result(result): - print "Auth OK" - print "result" + print result + return result def cbShutDown(ignored): reactor.stop() + def auth_eb(failure): + print "[ERROR!]", failure.getErrorMessage() + d = session.authenticate() d.addCallback(print_result) - d.addErrback(lambda f: log.err(f)) + d.addErrback(auth_eb) + d.addCallback(lambda _: session.logout()) d.addBoth(cbShutDown) reactor.run() diff --git a/bonafide/src/leap/bonafide/srp_auth.py b/bonafide/src/leap/bonafide/srp_auth.py index ac2cd67..d48214f 100644 --- a/bonafide/src/leap/bonafide/srp_auth.py +++ b/bonafide/src/leap/bonafide/srp_auth.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# srp.py -# Copyright (C) 2014 LEAP +# srp_auth.py +# Copyright (C) 2015 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,12 +20,10 @@ SRP Authentication. """ import binascii -import logging import json import srp -logger = logging.getLogger(__name__) class SRPAuthMechanism(object): @@ -54,11 +52,12 @@ class SRPAuthMechanism(object): return M def get_authentication_params(self, M, A): - # I think A is not used in the server side + # It looks A is not used server side return {'client_auth': binascii.hexlify(M), 'A': binascii.hexlify(A)} def process_authentication(self, authentication_response): auth = json.loads(authentication_response) + self._check_for_errors(auth) uuid = auth.get('id', None) token = auth.get('token', None) M2 = auth.get('M2', None) @@ -70,9 +69,9 @@ class SRPAuthMechanism(object): srp_user.verify_session(unhex_M2) assert srp_user.authenticated() - def _check_for_errors(self, challenge): - if 'errors' in challenge: - msg = challenge['errors']['base'] + def _check_for_errors(self, response): + if 'errors' in response: + msg = response['errors']['base'] raise SRPAuthError(msg) def _unhex_salt_B(self, salt, B): @@ -89,14 +88,9 @@ class SRPAuthMechanism(object): def _check_auth_params(self, uuid, token, M2): if not all((uuid, token, M2)): - msg = '%r' % (M2, uuid, token,) + msg = '%s' % str((M2, uuid, token)) raise SRPAuthBadDataFromServer(msg) - #XXX move to session ----------------------- - def get_session_id(self, cookies): - return cookies.get('_session_id', None) - #XXX move to session ----------------------- - def _safe_unhexlify(val): return binascii.unhexlify(val) \ @@ -116,5 +110,6 @@ class SRPAuthNoSalt(SRPAuthError): class SRPAuthNoB(SRPAuthError): message = 'The server didn\'t send the B parameter' + class SRPAuthBadDataFromServer(SRPAuthError): pass -- cgit v1.2.3 From 7576725b8992b621614e725c7a5a1c3b6991303a Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 4 Sep 2015 02:18:51 -0400 Subject: smtp certs [WIP] --- bonafide/src/leap/bonafide/_decorators.py | 2 +- bonafide/src/leap/bonafide/_http.py | 6 +++++- bonafide/src/leap/bonafide/provider.py | 5 +++++ bonafide/src/leap/bonafide/session.py | 30 +++++++++++++++++++++++------- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/bonafide/src/leap/bonafide/_decorators.py b/bonafide/src/leap/bonafide/_decorators.py index cfd6ec0..6b43715 100644 --- a/bonafide/src/leap/bonafide/_decorators.py +++ b/bonafide/src/leap/bonafide/_decorators.py @@ -19,7 +19,7 @@ Decorators used in bonafide. """ -def needs_authentication(func): +def auth_required(func): """ Decorate a method so that it will not be called if the instance attribute `is_authenticated` does not evaluate to True. diff --git a/bonafide/src/leap/bonafide/_http.py b/bonafide/src/leap/bonafide/_http.py index 6510e84..39aabab 100644 --- a/bonafide/src/leap/bonafide/_http.py +++ b/bonafide/src/leap/bonafide/_http.py @@ -18,6 +18,7 @@ """ twisted.web utils for bonafide. """ +import base64 import cookielib import urllib @@ -39,12 +40,15 @@ def cookieAgentFactory(verify_path, connectTimeout=30): return CookieAgent(agent, cookiejar) -def httpRequest(agent, url, values={}, headers={}, method='POST'): +def httpRequest(agent, url, values={}, headers={}, method='POST', token=None): data = '' if values: data = urllib.urlencode(values) headers['Content-Type'] = ['application/x-www-form-urlencoded'] + if token: + headers['Authorization'] = ['Token token="%s"' % (bytes(token))] + def handle_response(response): if response.code == 204: d = defer.succeed('') diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py index ca2ea1d..5b13d73 100644 --- a/bonafide/src/leap/bonafide/provider.py +++ b/bonafide/src/leap/bonafide/provider.py @@ -21,8 +21,13 @@ LEAP Provider API. class LeapProviderApi(object): # TODO when should the provider-api object be created? + # TODO relate to a Provider object, with autoconf flag. # XXX separate in auth-needing actions? + # doing that in LeapSession right now (with a decorator) + # but probably it would be better if we can just gather that info in just + # one place and decorate the methods programatically. + # XXX version this mapping !!! actions = { diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index 85e49e0..198b250 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -21,7 +21,7 @@ from twisted.internet import defer, reactor from twisted.python import log from leap.bonafide import srp_auth -from leap.bonafide._decorators import needs_authentication +from leap.bonafide._decorators import auth_required from leap.bonafide._http import httpRequest, cookieAgentFactory @@ -57,8 +57,8 @@ class LeapSession(object): log.msg("%s to %s" % (method, uri)) params = self._srp_auth.get_handshake_params(self.username, A) - handshake = yield httpRequest(self._agent, uri, values=params, - method=method) + handshake = yield self._request(self._agent, uri, values=params, + method=method) M = self._srp_auth.process_handshake(srpuser, handshake) uri, method = self._api.get_uri_and_method( @@ -66,26 +66,38 @@ class LeapSession(object): log.msg("%s to %s" % (method, uri)) params = self._srp_auth.get_authentication_params(M, A) - auth = yield httpRequest(self._agent, uri, values=params, - method=method) + auth = yield self._request(self._agent, uri, values=params, + method=method) uuid, token, M2 = self._srp_auth.process_authentication(auth) self._srp_auth.verify_authentication(srpuser, M2) self._uuid = uuid self._token = token - defer.returnValue('[OK] Credentias Authenticated through SRP') + defer.returnValue('[OK] Credentials Authenticated through SRP') - @needs_authentication + @auth_required def logout(self): print "Should logout..." + @auth_required + def get_smtp_cert(self): + # TODO pass it to the provider object so that it can save it in the + # right path. + uri, method = self._api.get_uri_and_method('get_smtp_cert') + print method, "to", uri + return self._request(self._agent, uri, method=method) + @property def is_authenticated(self): if not self._srp_user: return False return self._srp_user.authenticated() + def _request(self, *args, **kw): + kw['token'] = self._token + return httpRequest(*args, **kw) + if __name__ == "__main__": from leap.bonafide import provider @@ -106,10 +118,14 @@ if __name__ == "__main__": def auth_eb(failure): print "[ERROR!]", failure.getErrorMessage() + log.err(failure) d = session.authenticate() d.addCallback(print_result) d.addErrback(auth_eb) + d.addCallback(lambda _: session.get_smtp_cert()) + d.addCallback(print_result) + d.addErrback(auth_eb) d.addCallback(lambda _: session.logout()) d.addBoth(cbShutDown) reactor.run() -- cgit v1.2.3 From 5deec1120fef68b9cd41b29eb6af974fe72aa9d5 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Sep 2015 01:08:36 -0400 Subject: move srp_auth to _srp --- bonafide/src/leap/bonafide/_srp.py | 145 +++++++++++++++++++++++++++++++++ bonafide/src/leap/bonafide/srp_auth.py | 115 -------------------------- 2 files changed, 145 insertions(+), 115 deletions(-) create mode 100644 bonafide/src/leap/bonafide/_srp.py delete mode 100644 bonafide/src/leap/bonafide/srp_auth.py diff --git a/bonafide/src/leap/bonafide/_srp.py b/bonafide/src/leap/bonafide/_srp.py new file mode 100644 index 0000000..dc856ab --- /dev/null +++ b/bonafide/src/leap/bonafide/_srp.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# srp_auth.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +SRP Authentication. +""" + +import binascii +import json + +import srp + + +class SRPAuthMechanism(object): + + """ + Implement a protocol-agnostic SRP Authentication mechanism. + """ + + def initialize(self, username, password): + srp_user = srp.User(username.encode('utf-8'), + password.encode('utf-8'), + srp.SHA256, srp.NG_1024) + _, A = srp_user.start_authentication() + return srp_user, A + + def get_handshake_params(self, username, A): + return {'login': bytes(username), 'A': binascii.hexlify(A)} + + def process_handshake(self, srp_user, handshake_response): + challenge = json.loads(handshake_response) + self._check_for_errors(challenge) + salt = challenge.get('salt', None) + B = challenge.get('B', None) + unhex_salt, unhex_B = self._unhex_salt_B(salt, B) + M = srp_user.process_challenge(unhex_salt, unhex_B) + return M + + def get_authentication_params(self, M, A): + # It looks A is not used server side + return {'client_auth': binascii.hexlify(M), 'A': binascii.hexlify(A)} + + def process_authentication(self, authentication_response): + auth = json.loads(authentication_response) + self._check_for_errors(auth) + uuid = auth.get('id', None) + token = auth.get('token', None) + M2 = auth.get('M2', None) + self._check_auth_params(uuid, token, M2) + return uuid, token, M2 + + def verify_authentication(self, srp_user, M2): + unhex_M2 = _safe_unhexlify(M2) + srp_user.verify_session(unhex_M2) + assert srp_user.authenticated() + + def _check_for_errors(self, response): + if 'errors' in response: + msg = response['errors']['base'] + raise SRPAuthError(msg) + + def _unhex_salt_B(self, salt, B): + if salt is None: + raise SRPAuthNoSalt() + if B is None: + raise SRPAuthNoB() + try: + unhex_salt = _safe_unhexlify(salt) + unhex_B = _safe_unhexlify(B) + except (TypeError, ValueError) as e: + raise SRPAuthBadDataFromServer(str(e)) + return unhex_salt, unhex_B + + def _check_auth_params(self, uuid, token, M2): + if not all((uuid, token, M2)): + msg = '%s' % str((M2, uuid, token)) + raise SRPAuthBadDataFromServer(msg) + + +class SRPSignupMechanism(object): + + """ + Implement a protocol-agnostic SRP Registration mechanism. + """ + + def get_signup_params(self, username, password): + salt, verifier = srp.create_salted_verification_key( + bytes(username), bytes(password), + srp.SHA256, srp.NG_1024) + user_data = { + 'user[login]': username, + 'user[password_salt]': binascii.hexlify(salt), + 'user[password_verifier]': binascii.hexlify(verifier)} + return user_data + + def process_signup(self, signup_response): + signup = json.loads(signup_response) + errors = signup.get('errors') + if errors: + msg = 'username ' + errors.get('login')[0] + raise SRPRegistrationError(msg) + else: + username = signup.get('login') + return username + + +def _safe_unhexlify(val): + return binascii.unhexlify(val) \ + if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) + + +class SRPAuthError(Exception): + """ + Base exception for srp authentication errors + """ + + +class SRPAuthNoSalt(SRPAuthError): + message = 'The server didn\'t send the salt parameter' + + +class SRPAuthNoB(SRPAuthError): + message = 'The server didn\'t send the B parameter' + + +class SRPAuthBadDataFromServer(SRPAuthError): + pass + +class SRPRegistrationError(Exception): + pass + diff --git a/bonafide/src/leap/bonafide/srp_auth.py b/bonafide/src/leap/bonafide/srp_auth.py deleted file mode 100644 index d48214f..0000000 --- a/bonafide/src/leap/bonafide/srp_auth.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -# srp_auth.py -# Copyright (C) 2015 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -SRP Authentication. -""" - -import binascii -import json - -import srp - - - -class SRPAuthMechanism(object): - - """ - Implement a protocol-agnostic SRP Authentication mechanism. - """ - - def initialize(self, username, password): - srp_user = srp.User(username.encode('utf-8'), - password.encode('utf-8'), - srp.SHA256, srp.NG_1024) - _, A = srp_user.start_authentication() - return srp_user, A - - def get_handshake_params(self, username, A): - return {'login': bytes(username), 'A': binascii.hexlify(A)} - - def process_handshake(self, srp_user, handshake_response): - challenge = json.loads(handshake_response) - self._check_for_errors(challenge) - salt = challenge.get('salt', None) - B = challenge.get('B', None) - unhex_salt, unhex_B = self._unhex_salt_B(salt, B) - M = srp_user.process_challenge(unhex_salt, unhex_B) - return M - - def get_authentication_params(self, M, A): - # It looks A is not used server side - return {'client_auth': binascii.hexlify(M), 'A': binascii.hexlify(A)} - - def process_authentication(self, authentication_response): - auth = json.loads(authentication_response) - self._check_for_errors(auth) - uuid = auth.get('id', None) - token = auth.get('token', None) - M2 = auth.get('M2', None) - self._check_auth_params(uuid, token, M2) - return uuid, token, M2 - - def verify_authentication(self, srp_user, M2): - unhex_M2 = _safe_unhexlify(M2) - srp_user.verify_session(unhex_M2) - assert srp_user.authenticated() - - def _check_for_errors(self, response): - if 'errors' in response: - msg = response['errors']['base'] - raise SRPAuthError(msg) - - def _unhex_salt_B(self, salt, B): - if salt is None: - raise SRPAuthNoSalt() - if B is None: - raise SRPAuthNoB() - try: - unhex_salt = _safe_unhexlify(salt) - unhex_B = _safe_unhexlify(B) - except (TypeError, ValueError) as e: - raise SRPAuthBadDataFromServer(str(e)) - return unhex_salt, unhex_B - - def _check_auth_params(self, uuid, token, M2): - if not all((uuid, token, M2)): - msg = '%s' % str((M2, uuid, token)) - raise SRPAuthBadDataFromServer(msg) - - -def _safe_unhexlify(val): - return binascii.unhexlify(val) \ - if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) - - -class SRPAuthError(Exception): - """ - Base exception for srp authentication errors - """ - - -class SRPAuthNoSalt(SRPAuthError): - message = 'The server didn\'t send the salt parameter' - - -class SRPAuthNoB(SRPAuthError): - message = 'The server didn\'t send the B parameter' - - -class SRPAuthBadDataFromServer(SRPAuthError): - pass -- cgit v1.2.3 From 0a1bd88326e0dc6e4c517ed93e82043f6b092c95 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Sep 2015 01:58:02 -0400 Subject: add dynamic dispatcher for actions --- bonafide/src/leap/bonafide/provider.py | 130 ++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 28 deletions(-) diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py index 5b13d73..470b413 100644 --- a/bonafide/src/leap/bonafide/provider.py +++ b/bonafide/src/leap/bonafide/provider.py @@ -14,50 +14,124 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + """ LEAP Provider API. """ +from copy import deepcopy +import re +from urlparse import urlparse -class LeapProviderApi(object): - # TODO when should the provider-api object be created? - # TODO relate to a Provider object, with autoconf flag. - # XXX separate in auth-needing actions? - # doing that in LeapSession right now (with a decorator) - # but probably it would be better if we can just gather that info in just - # one place and decorate the methods programatically. +class _MetaActionDispatcher(type): + + """ + A metaclass that will create dispatcher methods dynamically for each + action made available by the LEAP provider API. + + The new methods will be created according to the values contained in an + `_actions` dictionary, with the following format:: + + {'action_name': (uri_template, method)} + + where `uri_template` is a string that will be formatted with an arbitrary + number of keyword arguments. + + Any class that uses this one as its metaclass needs to implement two private + methods:: + + _get_uri(self, action_name, **extra_params) + _get_method(self, action_name) + """ + + def __new__(meta, name, bases, dct): + + def _generate_action_funs(dct): + _get_uri = dct['_get_uri'] + _get_method = dct['_get_method'] + newdct = deepcopy(dct) + actions = dct['_actions'] + + def create_uri_fun(action_name): + return lambda self, **kw: _get_uri( + self, action_name=action_name, **kw) + + def create_met_fun(action_name): + return lambda self: _get_method( + self, action_name=action_name) - # XXX version this mapping !!! + for action in actions: + uri, method = actions[action] + _action_uri = 'get_%s_uri' % action + _action_met = 'get_%s_method' % action + newdct[_action_uri] = create_uri_fun(action) + newdct[_action_met] = create_met_fun(action) + return newdct - actions = { + newdct = _generate_action_funs(dct) + return super(_MetaActionDispatcher, meta).__new__( + meta, name, bases, newdct) + + +class Api(object): + """ + An object that has all the information that a client needs to communicate + with the remote methods exposed by the web API of a LEAP provider. + + By using the _MetaActionDispatcher as a metaclass, the _actions dict will be + translated dynamically into a set of instance methods that will allow + getting the uri and method for each action. + + The keyword arguments specified in the format string will automatically + raise a KeyError if the needed keyword arguments are not passed to the + dynamically created methods. + """ + + # TODO when should the provider-api object be created? + # TODO pass a Provider object to constructor, with autoconf flag. + # TODO make the actions attribute depend on the api version + + __metaclass__ = _MetaActionDispatcher + _actions = { 'signup': ('users', 'POST'), + 'update_user': ('users/{uid}', 'PUT'), 'handshake': ('sessions', 'POST'), 'authenticate': ('sessions/{login}', 'PUT'), - 'update_user': ('users/{uid}', 'PUT'), 'logout': ('logout', 'DELETE'), - 'get_vpn_cert': ('cert', 'POST'), - 'get_smtp_cert': ('smtp_cert', 'POST'), + 'vpn_cert': ('cert', 'POST'), + 'smtp_cert': ('smtp_cert', 'POST'), } - def __init__(self, uri, version): - self.uri = uri + def __init__(self, netloc, version=1): + parsed = urlparse(netloc) + if parsed.scheme != 'https': + raise ValueError( + 'ProviderApi needs to be passed a url with https scheme') + self.netloc = parsed.netloc self.version = version - @property - def base_url(self): - return "https://{0}/{1}".format(self.uri, self.version) + def get_hostname(self): + return urlparse(self._get_base_url()).hostname + + def _get_base_url(self): + return "https://{0}/{1}".format(self.netloc, self.version) + + # Methods expected by the dispatcher metaclass + + def _get_uri(self, action_name, **extra_params): + resource, _ = self._actions.get(action_name) + uri = '{0}/{1}'.format( + bytes(self._get_base_url()), + bytes(resource)).format(**extra_params) + return uri - # XXX split in two different methods? - def get_uri_and_method(self, action_name, **extra_params): - action = self.actions.get(action_name, None) - if not action: - raise ValueError("Requested a non-existent action for this API") - resource, method = action + def _get_method(self, action_name): + _, method = self._actions.get(action_name) + return method - uri = '{0}/{1}'.format(bytes(self.base_url), bytes(resource)).format( - **extra_params) - return uri, method - # XXX add a provider_domain property, just to check if it's the right - # provider domain? +def validate_username(username): + accepted_characters = '^[a-z0-9\-\_\.]*$' + if not re.match(accepted_characters, username): + raise ValueError('Only lowercase letters, digits, . - and _ allowed.') -- cgit v1.2.3 From 17348347a08ba28578e2efdc925eac06a8cad822 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Sep 2015 01:58:37 -0400 Subject: add colorama --- bonafide/pkg/requirements.pip | 1 + 1 file changed, 1 insertion(+) diff --git a/bonafide/pkg/requirements.pip b/bonafide/pkg/requirements.pip index d9bbc3f..a8c4ffe 100644 --- a/bonafide/pkg/requirements.pip +++ b/bonafide/pkg/requirements.pip @@ -1,3 +1,4 @@ srp twisted>=14.0.0 service-identity +colorama -- cgit v1.2.3 From a77c850ca00716363700faa0fda79747921004e2 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Sep 2015 01:59:26 -0400 Subject: use dynamic dipatcher; add signup --- bonafide/src/leap/bonafide/session.py | 114 +++++++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 31 deletions(-) diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index 198b250..6b60b6a 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -20,12 +20,15 @@ LEAP Session management. from twisted.internet import defer, reactor from twisted.python import log -from leap.bonafide import srp_auth +from leap.bonafide import _srp +from leap.bonafide import provider from leap.bonafide._decorators import auth_required from leap.bonafide._http import httpRequest, cookieAgentFactory +OK = 'ok' -class LeapSession(object): + +class Session(object): def __init__(self, credentials, api, provider_cert): # TODO check if an anonymous credentials is passed. @@ -39,75 +42,126 @@ class LeapSession(object): self.username = credentials.username self.password = credentials.password + self._provider_cert = provider_cert self._api = api + self._initialize_session() - self._agent = cookieAgentFactory(provider_cert) - self._srp_auth = srp_auth.SRPAuthMechanism() + def _initialize_session(self): + self._agent = cookieAgentFactory(self._provider_cert) + self._srp_auth = _srp.SRPAuthMechanism() + self._srp_signup = _srp.SRPSignupMechanism() self._srp_user = None self._token = None self._uuid = None + # Session + + @property + def token(self): + return self._token + + @property + def is_authenticated(self): + if not self._srp_user: + return False + return self._srp_user.authenticated() + @defer.inlineCallbacks def authenticate(self): srpuser, A = self._srp_auth.initialize( self.username, self.password) self._srp_user = srpuser - uri, method = self._api.get_uri_and_method('handshake') - log.msg("%s to %s" % (method, uri)) + uri = self._api.get_handshake_uri() + met = self._api.get_handshake_method() + log.msg("%s to %s" % (met, uri)) params = self._srp_auth.get_handshake_params(self.username, A) handshake = yield self._request(self._agent, uri, values=params, - method=method) + method=met) M = self._srp_auth.process_handshake(srpuser, handshake) - uri, method = self._api.get_uri_and_method( - 'authenticate', login=self.username) - log.msg("%s to %s" % (method, uri)) + uri = self._api.get_authenticate_uri(login=self.username) + met = self._api.get_authenticate_method() + + log.msg("%s to %s" % (met, uri)) params = self._srp_auth.get_authentication_params(M, A) auth = yield self._request(self._agent, uri, values=params, - method=method) + method=met) uuid, token, M2 = self._srp_auth.process_authentication(auth) self._srp_auth.verify_authentication(srpuser, M2) self._uuid = uuid self._token = token - defer.returnValue('[OK] Credentials Authenticated through SRP') + defer.returnValue(OK) @auth_required def logout(self): - print "Should logout..." + self.username = None + self.password = None + self._initialize_session() + + # User certificates + + def get_vpn_cert(self): + # TODO pass it to the provider object so that it can save it in the + # right path. + uri = self._api.get_vpn_cert_uri() + met = self._api.get_vpn_cert_method() + return self._request(self._agent, uri, method=met) @auth_required def get_smtp_cert(self): # TODO pass it to the provider object so that it can save it in the # right path. - uri, method = self._api.get_uri_and_method('get_smtp_cert') - print method, "to", uri - return self._request(self._agent, uri, method=method) - - @property - def is_authenticated(self): - if not self._srp_user: - return False - return self._srp_user.authenticated() + uri = self._api.get_smtp_cert_uri() + met = self._api.get_smtp_cert_method() + print met, "to", uri + return self._request(self._agent, uri, method=met) def _request(self, *args, **kw): kw['token'] = self._token return httpRequest(*args, **kw) + # User management + + @defer.inlineCallbacks + def signup(self, username, password): + # XXX should check that it_IS_NOT_authenticated + provider.validate_username(username) + uri = self._api.get_signup_uri() + met = self._api.get_signup_method() + params = self._srp_signup.get_signup_params( + username, password) + + signup = yield self._request(self._agent, uri, values=params, + method=met) + registered_user = self._srp_signup.process_signup(signup) + self.username = username + self.password = password + defer.returnValue((OK, registered_user)) + + @auth_required + def update_user_record(self): + pass + if __name__ == "__main__": - from leap.bonafide import provider + import os + import sys from twisted.cred.credentials import UsernamePassword - api = provider.LeapProviderApi('api.cdev.bitmask.net:4430', 1) - credentials = UsernamePassword('test_deb_090', 'lalalala') - - cdev_pem = '/home/kali/.config/leap/providers/cdev.bitmask.net/keys/ca/cacert.pem' - session = LeapSession(credentials, api, cdev_pem) + if len(sys.argv) != 4: + print "Usage:", sys.argv[0], "provider", "username", "password" + sys.exit() + _provider, username, password = sys.argv[1], sys.argv[2], sys.argv[3] + api = provider.Api('https://api.%s:4430' % _provider) + credentials = UsernamePassword(username, password) + cdev_pem = os.path.expanduser( + '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) + session = Session(credentials, api, cdev_pem) def print_result(result): print result @@ -123,9 +177,7 @@ if __name__ == "__main__": d = session.authenticate() d.addCallback(print_result) d.addErrback(auth_eb) - d.addCallback(lambda _: session.get_smtp_cert()) - d.addCallback(print_result) - d.addErrback(auth_eb) d.addCallback(lambda _: session.logout()) + d.addErrback(auth_eb) d.addBoth(cbShutDown) reactor.run() -- cgit v1.2.3 From 085e5ffa868aee3c2852a951336046e6d107e3f3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Sep 2015 01:59:57 -0400 Subject: ignore .project --- bonafide/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/bonafide/.gitignore b/bonafide/.gitignore index 0e26c09..9ac4b93 100644 --- a/bonafide/.gitignore +++ b/bonafide/.gitignore @@ -3,6 +3,7 @@ *.egg-info *.swp *.swo +.project dist/ build/ MANIFEST -- cgit v1.2.3 From 6371dd9282b04a5496dae1a6f7b25a403989c167 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Sep 2015 02:00:21 -0400 Subject: add simple bonafide_cli for tests --- bonafide/src/leap/bonafide/bonafide_cli.py | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 bonafide/src/leap/bonafide/bonafide_cli.py diff --git a/bonafide/src/leap/bonafide/bonafide_cli.py b/bonafide/src/leap/bonafide/bonafide_cli.py new file mode 100644 index 0000000..43c8ced --- /dev/null +++ b/bonafide/src/leap/bonafide/bonafide_cli.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# bonafide_cli.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Command Line interface for bonafide cli. +""" + +# XXX Warning! Work in progress------------------------------------------------ +# This is just a demo, real cli will work against a persistent bonafide daemon. +# XXX ------------------------------------------------------------------------- + +import argparse +import os +import sys +from getpass import getpass + +from colorama import init as color_init +from colorama import Fore + +from twisted.cred.credentials import UsernamePassword +from twisted.internet import reactor + +from leap.bonafide import provider +from leap.bonafide import session + +COMMANDS = ('signup', 'authenticate') + + +def _cbShutDown(ignored): + reactor.stop() + + +def _authEb(failure): + print(Fore.RED + "[error] " + Fore.YELLOW + + failure.getErrorMessage() + Fore.RESET) + + +def _display_token(result, _session): + if result == session.OK: + print('[ok] token--> ' + Fore.GREEN + + _session.token + Fore.RESET) + +def _display_registered(result, _session, _provider): + ok, user = result + if ok == session.OK: + print('[ok] registered username--> ' + Fore.GREEN + + '%s@%s' % (user, _provider)) + + +def run_command(command, _provider, username, password): + api = provider.Api('https://api.%s:4430' % _provider) + credentials = UsernamePassword(username, password) + cdev_pem = os.path.expanduser( + '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) + _session = session.Session(credentials, api, cdev_pem) + + if command == 'authenticate': + d = _session.authenticate() + d.addCallback(_display_token, _session) + elif command == 'signup': + d = _session.signup(username, password) + d.addCallback(_display_registered, _session, _provider) + else: + print(Fore.YELLOW + "Command not implemented" + Fore.RESET) + sys.exit() + + d.addErrback(_authEb) + d.addCallback(lambda _: _session.logout()) + d.addBoth(_cbShutDown) + reactor.run() + +if __name__ == '__main__': + color_init() + description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' + 'using the bonafide protocol.' + Fore.RESET) + parser = argparse.ArgumentParser(description=description) + parser.add_argument('command', type=str, choices=COMMANDS) + parser.add_argument('--provider', dest='provider', required=True) + parser.add_argument('--username', dest='username', required=True) + + ns = parser.parse_args() + password = getpass( + Fore.BLUE + '%s@%s password:' % (ns.username, ns.provider) + Fore.RESET) + run_command(ns.command, ns.provider, ns.username, password) -- cgit v1.2.3 From ca925d5033383b64e719775fc34a6c36e9672122 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Sep 2015 02:03:10 -0400 Subject: move to bonafide_cli --- bonafide/src/leap/bonafide/bonafide_cli | 98 ++++++++++++++++++++++++++++++ bonafide/src/leap/bonafide/bonafide_cli.py | 97 ----------------------------- 2 files changed, 98 insertions(+), 97 deletions(-) create mode 100755 bonafide/src/leap/bonafide/bonafide_cli delete mode 100644 bonafide/src/leap/bonafide/bonafide_cli.py diff --git a/bonafide/src/leap/bonafide/bonafide_cli b/bonafide/src/leap/bonafide/bonafide_cli new file mode 100755 index 0000000..64aa6ad --- /dev/null +++ b/bonafide/src/leap/bonafide/bonafide_cli @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# bonafide_cli.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Command Line interface for bonafide cli. +""" + +# XXX Warning! Work in progress------------------------------------------------ +# This is just a demo, real cli will work against a persistent bonafide daemon. +# XXX ------------------------------------------------------------------------- + +import argparse +import os +import sys +from getpass import getpass + +from colorama import init as color_init +from colorama import Fore + +from twisted.cred.credentials import UsernamePassword +from twisted.internet import reactor + +from leap.bonafide import provider +from leap.bonafide import session + +COMMANDS = ('signup', 'authenticate') + + +def _cbShutDown(ignored): + reactor.stop() + + +def _authEb(failure): + print(Fore.RED + "[error] " + Fore.YELLOW + + failure.getErrorMessage() + Fore.RESET) + + +def _display_token(result, _session): + if result == session.OK: + print('[ok] token--> ' + Fore.GREEN + + _session.token + Fore.RESET) + +def _display_registered(result, _session, _provider): + ok, user = result + if ok == session.OK: + print('[ok] registered username--> ' + Fore.GREEN + + '%s@%s' % (user, _provider)) + + +def run_command(command, _provider, username, password): + api = provider.Api('https://api.%s:4430' % _provider) + credentials = UsernamePassword(username, password) + cdev_pem = os.path.expanduser( + '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) + _session = session.Session(credentials, api, cdev_pem) + + if command == 'authenticate': + d = _session.authenticate() + d.addCallback(_display_token, _session) + elif command == 'signup': + d = _session.signup(username, password) + d.addCallback(_display_registered, _session, _provider) + else: + print(Fore.YELLOW + "Command not implemented" + Fore.RESET) + sys.exit() + + d.addErrback(_authEb) + d.addCallback(lambda _: _session.logout()) + d.addBoth(_cbShutDown) + reactor.run() + +if __name__ == '__main__': + color_init() + description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' + 'using the bonafide protocol.' + Fore.RESET) + parser = argparse.ArgumentParser(description=description) + parser.add_argument('command', type=str, choices=COMMANDS) + parser.add_argument('--provider', dest='provider', required=True) + parser.add_argument('--username', dest='username', required=True) + + ns = parser.parse_args() + password = getpass( + Fore.BLUE + '%s@%s password:' % (ns.username, ns.provider) + Fore.RESET) + run_command(ns.command, ns.provider, ns.username, password) diff --git a/bonafide/src/leap/bonafide/bonafide_cli.py b/bonafide/src/leap/bonafide/bonafide_cli.py deleted file mode 100644 index 43c8ced..0000000 --- a/bonafide/src/leap/bonafide/bonafide_cli.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -# bonafide_cli.py -# Copyright (C) 2015 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Command Line interface for bonafide cli. -""" - -# XXX Warning! Work in progress------------------------------------------------ -# This is just a demo, real cli will work against a persistent bonafide daemon. -# XXX ------------------------------------------------------------------------- - -import argparse -import os -import sys -from getpass import getpass - -from colorama import init as color_init -from colorama import Fore - -from twisted.cred.credentials import UsernamePassword -from twisted.internet import reactor - -from leap.bonafide import provider -from leap.bonafide import session - -COMMANDS = ('signup', 'authenticate') - - -def _cbShutDown(ignored): - reactor.stop() - - -def _authEb(failure): - print(Fore.RED + "[error] " + Fore.YELLOW + - failure.getErrorMessage() + Fore.RESET) - - -def _display_token(result, _session): - if result == session.OK: - print('[ok] token--> ' + Fore.GREEN + - _session.token + Fore.RESET) - -def _display_registered(result, _session, _provider): - ok, user = result - if ok == session.OK: - print('[ok] registered username--> ' + Fore.GREEN + - '%s@%s' % (user, _provider)) - - -def run_command(command, _provider, username, password): - api = provider.Api('https://api.%s:4430' % _provider) - credentials = UsernamePassword(username, password) - cdev_pem = os.path.expanduser( - '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) - _session = session.Session(credentials, api, cdev_pem) - - if command == 'authenticate': - d = _session.authenticate() - d.addCallback(_display_token, _session) - elif command == 'signup': - d = _session.signup(username, password) - d.addCallback(_display_registered, _session, _provider) - else: - print(Fore.YELLOW + "Command not implemented" + Fore.RESET) - sys.exit() - - d.addErrback(_authEb) - d.addCallback(lambda _: _session.logout()) - d.addBoth(_cbShutDown) - reactor.run() - -if __name__ == '__main__': - color_init() - description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' - 'using the bonafide protocol.' + Fore.RESET) - parser = argparse.ArgumentParser(description=description) - parser.add_argument('command', type=str, choices=COMMANDS) - parser.add_argument('--provider', dest='provider', required=True) - parser.add_argument('--username', dest='username', required=True) - - ns = parser.parse_args() - password = getpass( - Fore.BLUE + '%s@%s password:' % (ns.username, ns.provider) + Fore.RESET) - run_command(ns.command, ns.provider, ns.username, password) -- cgit v1.2.3 From 4ed4a4019c306879653549b3062fee61edee77b4 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Oct 2015 09:49:34 -0400 Subject: handle unicode errors --- bonafide/src/leap/bonafide/_http.py | 1 + bonafide/src/leap/bonafide/_srp.py | 6 ++---- bonafide/src/leap/bonafide/bonafide_cli | 2 ++ bonafide/src/leap/bonafide/provider.py | 2 ++ bonafide/src/leap/bonafide/session.py | 16 ++++++++++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/bonafide/src/leap/bonafide/_http.py b/bonafide/src/leap/bonafide/_http.py index 39aabab..b92cf57 100644 --- a/bonafide/src/leap/bonafide/_http.py +++ b/bonafide/src/leap/bonafide/_http.py @@ -50,6 +50,7 @@ def httpRequest(agent, url, values={}, headers={}, method='POST', token=None): headers['Authorization'] = ['Token token="%s"' % (bytes(token))] def handle_response(response): + print "RESPONSE CODE", response.code if response.code == 204: d = defer.succeed('') else: diff --git a/bonafide/src/leap/bonafide/_srp.py b/bonafide/src/leap/bonafide/_srp.py index dc856ab..4f4a605 100644 --- a/bonafide/src/leap/bonafide/_srp.py +++ b/bonafide/src/leap/bonafide/_srp.py @@ -32,8 +32,7 @@ class SRPAuthMechanism(object): """ def initialize(self, username, password): - srp_user = srp.User(username.encode('utf-8'), - password.encode('utf-8'), + srp_user = srp.User(username, password, srp.SHA256, srp.NG_1024) _, A = srp_user.start_authentication() return srp_user, A @@ -71,7 +70,7 @@ class SRPAuthMechanism(object): def _check_for_errors(self, response): if 'errors' in response: msg = response['errors']['base'] - raise SRPAuthError(msg) + raise SRPAuthError(unicode(msg).encode('utf-8')) def _unhex_salt_B(self, salt, B): if salt is None: @@ -142,4 +141,3 @@ class SRPAuthBadDataFromServer(SRPAuthError): class SRPRegistrationError(Exception): pass - diff --git a/bonafide/src/leap/bonafide/bonafide_cli b/bonafide/src/leap/bonafide/bonafide_cli index 64aa6ad..15a7a00 100755 --- a/bonafide/src/leap/bonafide/bonafide_cli +++ b/bonafide/src/leap/bonafide/bonafide_cli @@ -53,6 +53,8 @@ def _display_token(result, _session): if result == session.OK: print('[ok] token--> ' + Fore.GREEN + _session.token + Fore.RESET) + print('[ok] uuid --> ' + Fore.GREEN + + _session.uuid + Fore.RESET) def _display_registered(result, _session, _provider): ok, user = result diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py index 470b413..c22be3c 100644 --- a/bonafide/src/leap/bonafide/provider.py +++ b/bonafide/src/leap/bonafide/provider.py @@ -79,6 +79,8 @@ class Api(object): An object that has all the information that a client needs to communicate with the remote methods exposed by the web API of a LEAP provider. + The actions are described in https://leap.se/bonafide + By using the _MetaActionDispatcher as a metaclass, the _actions dict will be translated dynamically into a set of instance methods that will allow getting the uri and method for each action. diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index 6b60b6a..4fa3299 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -60,6 +60,10 @@ class Session(object): def token(self): return self._token + @property + def uuid(self): + return self._uuid + @property def is_authenticated(self): if not self._srp_user: @@ -98,10 +102,16 @@ class Session(object): defer.returnValue(OK) @auth_required + @defer.inlineCallbacks def logout(self): + uri = self._api.get_logout_uri() + met = self._api.get_logout_method() + auth = yield self._request(self._agent, uri, method=met) + print "AUTH RESULT->", auth self.username = None self.password = None self._initialize_session() + defer.returnValue(OK) # User certificates @@ -177,6 +187,12 @@ if __name__ == "__main__": d = session.authenticate() d.addCallback(print_result) d.addErrback(auth_eb) + + d.addCallback(lambda _: session.get_smtp_cert()) + #d.addCallback(lambda _: session.get_vpn_cert()) + d.addCallback(print_result) + d.addErrback(auth_eb) + d.addCallback(lambda _: session.logout()) d.addErrback(auth_eb) d.addBoth(cbShutDown) -- cgit v1.2.3 From b8ce26e69e15aa6f7786f7761ce30047d6a71906 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Oct 2015 09:53:38 -0400 Subject: README:Using --- bonafide/README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bonafide/README.rst b/bonafide/README.rst index 510dc28..af56016 100644 --- a/bonafide/README.rst +++ b/bonafide/README.rst @@ -1,6 +1,15 @@ Bonafide --------- +======== Secure user registration, authentication, and provider discovery for the LEAP applications. https://leap.se/en/docs/design/bonafide + +Using +----- +This is still in development. To play with it, create a virtualenv, and run:: + python setup.py development + +from the parent folder. + +Then you can use `bonafide_cli`. -- cgit v1.2.3 From d39d5d9ac81abaa2e0efde448f8bcf7f618a0292 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Oct 2015 09:57:42 -0400 Subject: add bonafide_cli entrypoint --- bonafide/setup.py | 5 ++ bonafide/src/leap/bonafide/bonafide_cli | 100 --------------------------- bonafide/src/leap/bonafide/bonafide_cli.py | 104 +++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 100 deletions(-) delete mode 100755 bonafide/src/leap/bonafide/bonafide_cli create mode 100755 bonafide/src/leap/bonafide/bonafide_cli.py diff --git a/bonafide/setup.py b/bonafide/setup.py index eef1541..828d82e 100644 --- a/bonafide/setup.py +++ b/bonafide/setup.py @@ -70,6 +70,8 @@ try: except Exception: long_description = "" +bonafide_cli = 'bonafide_cli=leap.bonafide.bonafide_cli:main' + setup( name='leap.bonafide', version=VERSION, @@ -92,4 +94,7 @@ setup( install_requires=parsed_reqs, include_package_data=True, zip_safe=False, + entry_points={ + 'console_scripts': [bonafide_cli] + }, ) diff --git a/bonafide/src/leap/bonafide/bonafide_cli b/bonafide/src/leap/bonafide/bonafide_cli deleted file mode 100755 index 15a7a00..0000000 --- a/bonafide/src/leap/bonafide/bonafide_cli +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# bonafide_cli.py -# Copyright (C) 2015 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Command Line interface for bonafide cli. -""" - -# XXX Warning! Work in progress------------------------------------------------ -# This is just a demo, real cli will work against a persistent bonafide daemon. -# XXX ------------------------------------------------------------------------- - -import argparse -import os -import sys -from getpass import getpass - -from colorama import init as color_init -from colorama import Fore - -from twisted.cred.credentials import UsernamePassword -from twisted.internet import reactor - -from leap.bonafide import provider -from leap.bonafide import session - -COMMANDS = ('signup', 'authenticate') - - -def _cbShutDown(ignored): - reactor.stop() - - -def _authEb(failure): - print(Fore.RED + "[error] " + Fore.YELLOW + - failure.getErrorMessage() + Fore.RESET) - - -def _display_token(result, _session): - if result == session.OK: - print('[ok] token--> ' + Fore.GREEN + - _session.token + Fore.RESET) - print('[ok] uuid --> ' + Fore.GREEN + - _session.uuid + Fore.RESET) - -def _display_registered(result, _session, _provider): - ok, user = result - if ok == session.OK: - print('[ok] registered username--> ' + Fore.GREEN + - '%s@%s' % (user, _provider)) - - -def run_command(command, _provider, username, password): - api = provider.Api('https://api.%s:4430' % _provider) - credentials = UsernamePassword(username, password) - cdev_pem = os.path.expanduser( - '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) - _session = session.Session(credentials, api, cdev_pem) - - if command == 'authenticate': - d = _session.authenticate() - d.addCallback(_display_token, _session) - elif command == 'signup': - d = _session.signup(username, password) - d.addCallback(_display_registered, _session, _provider) - else: - print(Fore.YELLOW + "Command not implemented" + Fore.RESET) - sys.exit() - - d.addErrback(_authEb) - d.addCallback(lambda _: _session.logout()) - d.addBoth(_cbShutDown) - reactor.run() - -if __name__ == '__main__': - color_init() - description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' - 'using the bonafide protocol.' + Fore.RESET) - parser = argparse.ArgumentParser(description=description) - parser.add_argument('command', type=str, choices=COMMANDS) - parser.add_argument('--provider', dest='provider', required=True) - parser.add_argument('--username', dest='username', required=True) - - ns = parser.parse_args() - password = getpass( - Fore.BLUE + '%s@%s password:' % (ns.username, ns.provider) + Fore.RESET) - run_command(ns.command, ns.provider, ns.username, password) diff --git a/bonafide/src/leap/bonafide/bonafide_cli.py b/bonafide/src/leap/bonafide/bonafide_cli.py new file mode 100755 index 0000000..30cae6a --- /dev/null +++ b/bonafide/src/leap/bonafide/bonafide_cli.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# bonafide_cli.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Command Line interface for bonafide cli. +""" + +# XXX Warning! Work in progress------------------------------------------------ +# This is just a demo, real cli will work against a persistent bonafide daemon. +# XXX ------------------------------------------------------------------------- + +import argparse +import os +import sys +from getpass import getpass + +from colorama import init as color_init +from colorama import Fore + +from twisted.cred.credentials import UsernamePassword +from twisted.internet import reactor + +from leap.bonafide import provider +from leap.bonafide import session + +COMMANDS = ('signup', 'authenticate') + + +def _cbShutDown(ignored): + reactor.stop() + + +def _authEb(failure): + print(Fore.RED + "[error] " + Fore.YELLOW + + failure.getErrorMessage() + Fore.RESET) + + +def _display_token(result, _session): + if result == session.OK: + print('[ok] token--> ' + Fore.GREEN + + _session.token + Fore.RESET) + print('[ok] uuid --> ' + Fore.GREEN + + _session.uuid + Fore.RESET) + +def _display_registered(result, _session, _provider): + ok, user = result + if ok == session.OK: + print('[ok] registered username--> ' + Fore.GREEN + + '%s@%s' % (user, _provider)) + + +def run_command(command, _provider, username, password): + api = provider.Api('https://api.%s:4430' % _provider) + credentials = UsernamePassword(username, password) + cdev_pem = os.path.expanduser( + '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) + _session = session.Session(credentials, api, cdev_pem) + + if command == 'authenticate': + d = _session.authenticate() + d.addCallback(_display_token, _session) + elif command == 'signup': + d = _session.signup(username, password) + d.addCallback(_display_registered, _session, _provider) + else: + print(Fore.YELLOW + "Command not implemented" + Fore.RESET) + sys.exit() + + d.addErrback(_authEb) + d.addCallback(lambda _: _session.logout()) + d.addBoth(_cbShutDown) + reactor.run() + +def main(): + color_init() + description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' + 'using the bonafide protocol.' + Fore.RESET) + parser = argparse.ArgumentParser(description=description) + parser.add_argument('command', type=str, choices=COMMANDS) + parser.add_argument('--provider', dest='provider', required=True) + parser.add_argument('--username', dest='username', required=True) + + ns = parser.parse_args() + password = getpass( + Fore.BLUE + '%s@%s password:' % (ns.username, ns.provider) + Fore.RESET) + run_command(ns.command, ns.provider, ns.username, password) + + +if __name__ == '__main__': + main() -- cgit v1.2.3 From 75277e3495e6280ae30e740800a07503deae44e2 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Oct 2015 10:00:45 -0400 Subject: add link to readme --- bonafide/README.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bonafide/README.rst b/bonafide/README.rst index af56016..ffcf2ff 100644 --- a/bonafide/README.rst +++ b/bonafide/README.rst @@ -1,15 +1,20 @@ Bonafide ======== -Secure user registration, authentication, and provider discovery for the LEAP -applications. +Bonafide is the protocol for secure user registration, authentication, and provider discovery for the LEAP +applications. See the `Bonafide`_ design docs. -https://leap.se/en/docs/design/bonafide +This is a client implementation, written in python. + +.. _`Bonafide`: https://leap.se/en/docs/design/bonafide Using ----- -This is still in development. To play with it, create a virtualenv, and run:: - python setup.py development + +This is still in development. To play with it, create a virtualenv, and deploy +the package in development mode by running:: + + python setup.py develop from the parent folder. -Then you can use `bonafide_cli`. +Then you can use `bonafide_cli -h` to see the available commands. -- cgit v1.2.3 From aa42a8b3823ac19c0dc20df1fef3b0296ad77bc0 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Oct 2015 11:26:35 -0400 Subject: add --skip-logout, to be able to reuse tokens --- bonafide/src/leap/bonafide/_http.py | 2 +- bonafide/src/leap/bonafide/bonafide_cli.py | 16 +++++++++++----- bonafide/src/leap/bonafide/session.py | 1 - 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/bonafide/src/leap/bonafide/_http.py b/bonafide/src/leap/bonafide/_http.py index b92cf57..8f05b42 100644 --- a/bonafide/src/leap/bonafide/_http.py +++ b/bonafide/src/leap/bonafide/_http.py @@ -50,7 +50,7 @@ def httpRequest(agent, url, values={}, headers={}, method='POST', token=None): headers['Authorization'] = ['Token token="%s"' % (bytes(token))] def handle_response(response): - print "RESPONSE CODE", response.code + # print "RESPONSE CODE", response.code if response.code == 204: d = defer.succeed('') else: diff --git a/bonafide/src/leap/bonafide/bonafide_cli.py b/bonafide/src/leap/bonafide/bonafide_cli.py index 30cae6a..3f500e4 100755 --- a/bonafide/src/leap/bonafide/bonafide_cli.py +++ b/bonafide/src/leap/bonafide/bonafide_cli.py @@ -63,7 +63,7 @@ def _display_registered(result, _session, _provider): '%s@%s' % (user, _provider)) -def run_command(command, _provider, username, password): +def run_command(command, _provider, username, password, skip_logout): api = provider.Api('https://api.%s:4430' % _provider) credentials = UsernamePassword(username, password) cdev_pem = os.path.expanduser( @@ -81,23 +81,29 @@ def run_command(command, _provider, username, password): sys.exit() d.addErrback(_authEb) - d.addCallback(lambda _: _session.logout()) + if not skip_logout: + d.addCallback(lambda _: _session.logout()) d.addBoth(_cbShutDown) reactor.run() + def main(): color_init() description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' - 'using the bonafide protocol.' + Fore.RESET) + 'using the bonafide protocol.' + Fore.RESET) parser = argparse.ArgumentParser(description=description) parser.add_argument('command', type=str, choices=COMMANDS) + parser.add_argument( + '--skip-logout', action="store_true", + help=("Skip logout. Use this if you want to re-use the token.")) parser.add_argument('--provider', dest='provider', required=True) parser.add_argument('--username', dest='username', required=True) ns = parser.parse_args() password = getpass( - Fore.BLUE + '%s@%s password:' % (ns.username, ns.provider) + Fore.RESET) - run_command(ns.command, ns.provider, ns.username, password) + Fore.BLUE + '%s@%s password:' % ( + ns.username, ns.provider) + Fore.RESET) + run_command(ns.command, ns.provider, ns.username, password, ns.skip_logout) if __name__ == '__main__': diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index 4fa3299..8343a91 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -107,7 +107,6 @@ class Session(object): uri = self._api.get_logout_uri() met = self._api.get_logout_method() auth = yield self._request(self._agent, uri, method=met) - print "AUTH RESULT->", auth self.username = None self.password = None self._initialize_session() -- cgit v1.2.3 From f320408c385cd2dbe18672d91b563337a500c9b7 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Oct 2015 11:26:55 -0400 Subject: chmod -x bonafide_cli.py --- bonafide/src/leap/bonafide/bonafide_cli.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 bonafide/src/leap/bonafide/bonafide_cli.py diff --git a/bonafide/src/leap/bonafide/bonafide_cli.py b/bonafide/src/leap/bonafide/bonafide_cli.py old mode 100755 new mode 100644 -- cgit v1.2.3 From 74a293f5c26ee5a4ab1b254b326809ef8f87602f Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 18 Nov 2015 11:58:59 -0400 Subject: [pkg] move to versioneer 0.15 --- bonafide/.gitattributes | 1 + bonafide/MANIFEST.in | 2 + bonafide/setup.cfg | 7 + bonafide/setup.py | 20 +- bonafide/src/leap/bonafide/__init__.py | 4 + bonafide/src/leap/bonafide/_version.py | 460 +++++++++ bonafide/versioneer.py | 1699 ++++++++++++++++++++++++++++++++ 7 files changed, 2178 insertions(+), 15 deletions(-) create mode 100644 bonafide/.gitattributes create mode 100644 bonafide/MANIFEST.in create mode 100644 bonafide/src/leap/bonafide/_version.py create mode 100644 bonafide/versioneer.py diff --git a/bonafide/.gitattributes b/bonafide/.gitattributes new file mode 100644 index 0000000..1dc749d --- /dev/null +++ b/bonafide/.gitattributes @@ -0,0 +1 @@ +src/leap/bonafide/_version.py export-subst diff --git a/bonafide/MANIFEST.in b/bonafide/MANIFEST.in new file mode 100644 index 0000000..3355edd --- /dev/null +++ b/bonafide/MANIFEST.in @@ -0,0 +1,2 @@ +include versioneer.py +include src/leap/bonafide/_version.py diff --git a/bonafide/setup.cfg b/bonafide/setup.cfg index 4a2ab2b..edf2450 100644 --- a/bonafide/setup.cfg +++ b/bonafide/setup.cfg @@ -8,3 +8,10 @@ ignore = E731 [flake8] exclude = versioneer.py,_version.py,*.egg,build,dist,docs ignore = E731 + +[versioneer] +VCS = git +style = pep440 +versionfile_source = src/leap/bonafide/_version.py +versionfile_build = leap/bonafide/_version.py +tag_prefix = diff --git a/bonafide/setup.py b/bonafide/setup.py index 828d82e..154ac7c 100644 --- a/bonafide/setup.py +++ b/bonafide/setup.py @@ -22,13 +22,7 @@ from setuptools import setup, find_packages from pkg import utils -# XXX TODO --- update to versioneer 0.15 --------------------- -#import versioneer -#versioneer.versionfile_source = 'src/leap/bonafide/_version.py' -#versioneer.versionfile_build = 'leap/common/_version.py' -#versioneer.tag_prefix = '' # tags are like 1.2.0 -#versioneer.parentdir_prefix = 'leap.bonafide-' -# -------------------------------------------------------------- +import versioneer parsed_reqs = utils.parse_requirements() @@ -49,10 +43,7 @@ trove_classifiers = [ DOWNLOAD_BASE = ('https://github.com/leapcode/bonafide/' 'archive/%s.tar.gz') -#_versions = versioneer.get_versions() -#VERSION = _versions['version'] -VERSION = '0.0.1' -#VERSION_FULL = _versions['full'] +VERSION = versioneer.get_version() DOWNLOAD_URL = "" # get the short version for the download url @@ -60,9 +51,8 @@ DOWNLOAD_URL = "" #if len(_version_short) > 0: #VERSION_SHORT = _version_short[0] #DOWNLOAD_URL = DOWNLOAD_BASE % VERSION_SHORT -# -#cmdclass = versioneer.get_cmdclass() -# ---------------------------------------------------------------- + +cmdclass = versioneer.get_cmdclass() try: long_description = open('README.rst').read() + '\n\n\n' + \ @@ -75,7 +65,7 @@ bonafide_cli = 'bonafide_cli=leap.bonafide.bonafide_cli:main' setup( name='leap.bonafide', version=VERSION, - #cmdclass=cmdclass, + cmdclass=cmdclass, url='https://leap.se/', #download_url=DOWNLOAD_URL, license='GPLv3+', diff --git a/bonafide/src/leap/bonafide/__init__.py b/bonafide/src/leap/bonafide/__init__.py index e69de29..74f4e66 100644 --- a/bonafide/src/leap/bonafide/__init__.py +++ b/bonafide/src/leap/bonafide/__init__.py @@ -0,0 +1,4 @@ + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions diff --git a/bonafide/src/leap/bonafide/_version.py b/bonafide/src/leap/bonafide/_version.py new file mode 100644 index 0000000..91fb65c --- /dev/null +++ b/bonafide/src/leap/bonafide/_version.py @@ -0,0 +1,460 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.15 (https://github.com/warner/python-versioneer) + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + pass + + +def get_config(): + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "None" + cfg.parentdir_prefix = "None" + cfg.versionfile_source = "src/leap/bonafide/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + pass + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + def decorate(f): + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + return None + return stdout + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with " + "prefix '%s'" % (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs-tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + # this runs 'git' from the root of the source tree. This only gets called + # if the git-archive 'subst' keywords were *not* expanded, and + # _version.py hasn't already been rewritten with a short version string, + # meaning we're inside a checked out source tree. + + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + raise NotThisMethod("no .git directory") + + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + # if there is a tag, this yields TAG-NUM-gHEX[-dirty] + # if there are no tags, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long"], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +def plus_or_dot(pieces): + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + # now build up version string, with post-release "local version + # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + # exceptions: + # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + # TAG[.post.devDISTANCE] . No -dirty + + # exceptions: + # 1: no tags. 0.post.devDISTANCE + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that + # .dev0 sorts backwards (a dirty tree will appear "older" than the + # corresponding clean one), but you shouldn't be releasing software with + # -dirty anyways. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty + # --always' + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty + # --always -long'. The distance/hash is unconditional. + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +def get_versions(): + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} diff --git a/bonafide/versioneer.py b/bonafide/versioneer.py new file mode 100644 index 0000000..c010f63 --- /dev/null +++ b/bonafide/versioneer.py @@ -0,0 +1,1699 @@ + +# Version: 0.15 + +""" +The Versioneer +============== + +* like a rocketeer, but for versions! +* https://github.com/warner/python-versioneer +* Brian Warner +* License: Public Domain +* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, and pypy +* [![Latest Version] +(https://pypip.in/version/versioneer/badge.svg?style=flat) +](https://pypi.python.org/pypi/versioneer/) +* [![Build Status] +(https://travis-ci.org/warner/python-versioneer.png?branch=master) +](https://travis-ci.org/warner/python-versioneer) + +This is a tool for managing a recorded version number in distutils-based +python projects. The goal is to remove the tedious and error-prone "update +the embedded version string" step from your release process. Making a new +release should be as easy as recording a new tag in your version-control +system, and maybe making new tarballs. + + +## Quick Install + +* `pip install versioneer` to somewhere to your $PATH +* add a `[versioneer]` section to your setup.cfg (see below) +* run `versioneer install` in your source tree, commit the results + +## Version Identifiers + +Source trees come from a variety of places: + +* a version-control system checkout (mostly used by developers) +* a nightly tarball, produced by build automation +* a snapshot tarball, produced by a web-based VCS browser, like github's + "tarball from tag" feature +* a release tarball, produced by "setup.py sdist", distributed through PyPI + +Within each source tree, the version identifier (either a string or a number, +this tool is format-agnostic) can come from a variety of places: + +* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows + about recent "tags" and an absolute revision-id +* the name of the directory into which the tarball was unpacked +* an expanded VCS keyword ($Id$, etc) +* a `_version.py` created by some earlier build step + +For released software, the version identifier is closely related to a VCS +tag. Some projects use tag names that include more than just the version +string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool +needs to strip the tag prefix to extract the version identifier. For +unreleased software (between tags), the version identifier should provide +enough information to help developers recreate the same tree, while also +giving them an idea of roughly how old the tree is (after version 1.2, before +version 1.3). Many VCS systems can report a description that captures this, +for example `git describe --tags --dirty --always` reports things like +"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the +0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has +uncommitted changes. + +The version identifier is used for multiple purposes: + +* to allow the module to self-identify its version: `myproject.__version__` +* to choose a name and prefix for a 'setup.py sdist' tarball + +## Theory of Operation + +Versioneer works by adding a special `_version.py` file into your source +tree, where your `__init__.py` can import it. This `_version.py` knows how to +dynamically ask the VCS tool for version information at import time. + +`_version.py` also contains `$Revision$` markers, and the installation +process marks `_version.py` to have this marker rewritten with a tag name +during the `git archive` command. As a result, generated tarballs will +contain enough information to get the proper version. + +To allow `setup.py` to compute a version too, a `versioneer.py` is added to +the top level of your source tree, next to `setup.py` and the `setup.cfg` +that configures it. This overrides several distutils/setuptools commands to +compute the version when invoked, and changes `setup.py build` and `setup.py +sdist` to replace `_version.py` with a small static file that contains just +the generated version data. + +## Installation + +First, decide on values for the following configuration variables: + +* `VCS`: the version control system you use. Currently accepts "git". + +* `style`: the style of version string to be produced. See "Styles" below for + details. Defaults to "pep440", which looks like + `TAG[+DISTANCE.gSHORTHASH[.dirty]]`. + +* `versionfile_source`: + + A project-relative pathname into which the generated version strings should + be written. This is usually a `_version.py` next to your project's main + `__init__.py` file, so it can be imported at runtime. If your project uses + `src/myproject/__init__.py`, this should be `src/myproject/_version.py`. + This file should be checked in to your VCS as usual: the copy created below + by `setup.py setup_versioneer` will include code that parses expanded VCS + keywords in generated tarballs. The 'build' and 'sdist' commands will + replace it with a copy that has just the calculated version string. + + This must be set even if your project does not have any modules (and will + therefore never import `_version.py`), since "setup.py sdist" -based trees + still need somewhere to record the pre-calculated version strings. Anywhere + in the source tree should do. If there is a `__init__.py` next to your + `_version.py`, the `setup.py setup_versioneer` command (described below) + will append some `__version__`-setting assignments, if they aren't already + present. + +* `versionfile_build`: + + Like `versionfile_source`, but relative to the build directory instead of + the source directory. These will differ when your setup.py uses + 'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`, + then you will probably have `versionfile_build='myproject/_version.py'` and + `versionfile_source='src/myproject/_version.py'`. + + If this is set to None, then `setup.py build` will not attempt to rewrite + any `_version.py` in the built tree. If your project does not have any + libraries (e.g. if it only builds a script), then you should use + `versionfile_build = None` and override `distutils.command.build_scripts` + to explicitly insert a copy of `versioneer.get_version()` into your + generated script. + +* `tag_prefix`: + + a string, like 'PROJECTNAME-', which appears at the start of all VCS tags. + If your tags look like 'myproject-1.2.0', then you should use + tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this + should be an empty string. + +* `parentdir_prefix`: + + a optional string, frequently the same as tag_prefix, which appears at the + start of all unpacked tarball filenames. If your tarball unpacks into + 'myproject-1.2.0', this should be 'myproject-'. To disable this feature, + just omit the field from your `setup.cfg`. + +This tool provides one script, named `versioneer`. That script has one mode, +"install", which writes a copy of `versioneer.py` into the current directory +and runs `versioneer.py setup` to finish the installation. + +To versioneer-enable your project: + +* 1: Modify your `setup.cfg`, adding a section named `[versioneer]` and + populating it with the configuration values you decided earlier (note that + the option names are not case-sensitive): + + ```` + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = "" + parentdir_prefix = myproject- + ```` + +* 2: Run `versioneer install`. This will do the following: + + * copy `versioneer.py` into the top of your source tree + * create `_version.py` in the right place (`versionfile_source`) + * modify your `__init__.py` (if one exists next to `_version.py`) to define + `__version__` (by calling a function from `_version.py`) + * modify your `MANIFEST.in` to include both `versioneer.py` and the + generated `_version.py` in sdist tarballs + + `versioneer install` will complain about any problems it finds with your + `setup.py` or `setup.cfg`. Run it multiple times until you have fixed all + the problems. + +* 3: add a `import versioneer` to your setup.py, and add the following + arguments to the setup() call: + + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), + +* 4: commit these changes to your VCS. To make sure you won't forget, + `versioneer install` will mark everything it touched for addition using + `git add`. Don't forget to add `setup.py` and `setup.cfg` too. + +## Post-Installation Usage + +Once established, all uses of your tree from a VCS checkout should get the +current version string. All generated tarballs should include an embedded +version string (so users who unpack them will not need a VCS tool installed). + +If you distribute your project through PyPI, then the release process should +boil down to two steps: + +* 1: git tag 1.0 +* 2: python setup.py register sdist upload + +If you distribute it through github (i.e. users use github to generate +tarballs with `git archive`), the process is: + +* 1: git tag 1.0 +* 2: git push; git push --tags + +Versioneer will report "0+untagged.NUMCOMMITS.gHASH" until your tree has at +least one tag in its history. + +## Version-String Flavors + +Code which uses Versioneer can learn about its version string at runtime by +importing `_version` from your main `__init__.py` file and running the +`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can +import the top-level `versioneer.py` and run `get_versions()`. + +Both functions return a dictionary with different flavors of version +information: + +* `['version']`: A condensed version string, rendered using the selected + style. This is the most commonly used value for the project's version + string. The default "pep440" style yields strings like `0.11`, + `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section + below for alternative styles. + +* `['full-revisionid']`: detailed revision identifier. For Git, this is the + full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". + +* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that + this is only accurate if run in a VCS checkout, otherwise it is likely to + be False or None + +* `['error']`: if the version string could not be computed, this will be set + to a string describing the problem, otherwise it will be None. It may be + useful to throw an exception in setup.py if this is set, to avoid e.g. + creating tarballs with a version string of "unknown". + +Some variants are more useful than others. Including `full-revisionid` in a +bug report should allow developers to reconstruct the exact code being tested +(or indicate the presence of local changes that should be shared with the +developers). `version` is suitable for display in an "about" box or a CLI +`--version` output: it can be easily compared against release notes and lists +of bugs fixed in various releases. + +The installer adds the following text to your `__init__.py` to place a basic +version in `YOURPROJECT.__version__`: + + from ._version import get_versions + __version__ = get_versions()['version'] + del get_versions + +## Styles + +The setup.cfg `style=` configuration controls how the VCS information is +rendered into a version string. + +The default style, "pep440", produces a PEP440-compliant string, equal to the +un-prefixed tag name for actual releases, and containing an additional "local +version" section with more detail for in-between builds. For Git, this is +TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags +--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the +tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and +that this commit is two revisions ("+2") beyond the "0.11" tag. For released +software (exactly equal to a known tag), the identifier will only contain the +stripped tag, e.g. "0.11". + +Other styles are available. See details.md in the Versioneer source tree for +descriptions. + +## Debugging + +Versioneer tries to avoid fatal errors: if something goes wrong, it will tend +to return a version of "0+unknown". To investigate the problem, run `setup.py +version`, which will run the version-lookup code in a verbose mode, and will +display the full contents of `get_versions()` (including the `error` string, +which may help identify what went wrong). + +## Updating Versioneer + +To upgrade your project to a new release of Versioneer, do the following: + +* install the new Versioneer (`pip install -U versioneer` or equivalent) +* edit `setup.cfg`, if necessary, to include any new configuration settings + indicated by the release notes +* re-run `versioneer install` in your source tree, to replace + `SRC/_version.py` +* commit any changed files + +### Upgrading to 0.15 + +Starting with this version, Versioneer is configured with a `[versioneer]` +section in your `setup.cfg` file. Earlier versions required the `setup.py` to +set attributes on the `versioneer` module immediately after import. The new +version will refuse to run (raising an exception during import) until you +have provided the necessary `setup.cfg` section. + +In addition, the Versioneer package provides an executable named +`versioneer`, and the installation process is driven by running `versioneer +install`. In 0.14 and earlier, the executable was named +`versioneer-installer` and was run without an argument. + +### Upgrading to 0.14 + +0.14 changes the format of the version string. 0.13 and earlier used +hyphen-separated strings like "0.11-2-g1076c97-dirty". 0.14 and beyond use a +plus-separated "local version" section strings, with dot-separated +components, like "0.11+2.g1076c97". PEP440-strict tools did not like the old +format, but should be ok with the new one. + +### Upgrading from 0.11 to 0.12 + +Nothing special. + +### Upgrading from 0.10 to 0.11 + +You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running +`setup.py setup_versioneer`. This will enable the use of additional +version-control systems (SVN, etc) in the future. + +## Future Directions + +This tool is designed to make it easily extended to other version-control +systems: all VCS-specific components are in separate directories like +src/git/ . The top-level `versioneer.py` script is assembled from these +components by running make-versioneer.py . In the future, make-versioneer.py +will take a VCS name as an argument, and will construct a version of +`versioneer.py` that is specific to the given VCS. It might also take the +configuration arguments that are currently provided manually during +installation by editing setup.py . Alternatively, it might go the other +direction and include code from all supported VCS systems, reducing the +number of intermediate scripts. + + +## License + +To make Versioneer easier to embed, all its code is hereby released into the +public domain. The `_version.py` that it creates is also in the public +domain. + +""" + +from __future__ import print_function +try: + import configparser +except ImportError: + import ConfigParser as configparser +import errno +import json +import os +import re +import subprocess +import sys + + +class VersioneerConfig: + pass + + +def get_root(): + # we require that all commands are run from the project root, i.e. the + # directory that contains setup.py, setup.cfg, and versioneer.py . + root = os.path.realpath(os.path.abspath(os.getcwd())) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + # allow 'python path/to/setup.py COMMAND' + root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") + raise VersioneerBadRootError(err) + try: + # Certain runtime workflows (setup.py install/develop in a setuptools + # tree) execute all dependencies in a single python process, so + # "versioneer" may be imported multiple times, and python's shared + # module-import table will cache the first one. So we can't use + # os.path.dirname(__file__), as that will find whichever + # versioneer.py was first imported, even in later projects. + me = os.path.realpath(os.path.abspath(__file__)) + if os.path.splitext(me)[0] != os.path.splitext(versioneer_py)[0]: + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(me), versioneer_py)) + except NameError: + pass + return root + + +def get_config_from_root(root): + # This might raise EnvironmentError (if setup.cfg is missing), or + # configparser.NoSectionError (if it lacks a [versioneer] section), or + # configparser.NoOptionError (if it lacks "VCS="). See the docstring at + # the top of versioneer.py for instructions on writing your setup.cfg . + setup_cfg = os.path.join(root, "setup.cfg") + parser = configparser.SafeConfigParser() + with open(setup_cfg, "r") as f: + parser.readfp(f) + VCS = parser.get("versioneer", "VCS") # mandatory + + def get(parser, name): + if parser.has_option("versioneer", name): + return parser.get("versioneer", name) + return None + cfg = VersioneerConfig() + cfg.VCS = VCS + cfg.style = get(parser, "style") or "" + cfg.versionfile_source = get(parser, "versionfile_source") + cfg.versionfile_build = get(parser, "versionfile_build") + cfg.tag_prefix = get(parser, "tag_prefix") + cfg.parentdir_prefix = get(parser, "parentdir_prefix") + cfg.verbose = get(parser, "verbose") + return cfg + + +class NotThisMethod(Exception): + pass + +# these dictionaries contain VCS-specific tools +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + def decorate(f): + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + return None + return stdout +LONG_VERSION_PY['git'] = ''' +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.15 (https://github.com/warner/python-versioneer) + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" + git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + pass + + +def get_config(): + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "%(STYLE)s" + cfg.tag_prefix = "%(TAG_PREFIX)s" + cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" + cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + pass + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + def decorate(f): + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %%s" %% dispcmd) + print(e) + return None + else: + if verbose: + print("unable to find command, tried %%s" %% (commands,)) + return None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% dispcmd) + return None + return stdout + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%%s', but '%%s' doesn't start with " + "prefix '%%s'" %% (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %%d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%%s', no digits" %% ",".join(refs-tags)) + if verbose: + print("likely tags: %%s" %% ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %%s" %% r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + # this runs 'git' from the root of the source tree. This only gets called + # if the git-archive 'subst' keywords were *not* expanded, and + # _version.py hasn't already been rewritten with a short version string, + # meaning we're inside a checked out source tree. + + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %%s" %% root) + raise NotThisMethod("no .git directory") + + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + # if there is a tag, this yields TAG-NUM-gHEX[-dirty] + # if there are no tags, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long"], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%%s'" + %% describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%%s' doesn't start with prefix '%%s'" + print(fmt %% (full_tag, tag_prefix)) + pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" + %% (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +def plus_or_dot(pieces): + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + # now build up version string, with post-release "local version + # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + # exceptions: + # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + # TAG[.post.devDISTANCE] . No -dirty + + # exceptions: + # 1: no tags. 0.post.devDISTANCE + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%%d" %% pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%%d" %% pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that + # .dev0 sorts backwards (a dirty tree will appear "older" than the + # corresponding clean one), but you shouldn't be releasing software with + # -dirty anyways. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + return rendered + + +def render_pep440_old(pieces): + # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty + # --always' + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty + # --always -long'. The distance/hash is unconditional. + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%%s'" %% style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +def get_versions(): + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} +''' + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs-tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + # this runs 'git' from the root of the source tree. This only gets called + # if the git-archive 'subst' keywords were *not* expanded, and + # _version.py hasn't already been rewritten with a short version string, + # meaning we're inside a checked out source tree. + + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + raise NotThisMethod("no .git directory") + + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + # if there is a tag, this yields TAG-NUM-gHEX[-dirty] + # if there are no tags, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long"], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +def do_vcs_install(manifest_in, versionfile_source, ipy): + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + files = [manifest_in, versionfile_source] + if ipy: + files.append(ipy) + try: + me = __file__ + if me.endswith(".pyc") or me.endswith(".pyo"): + me = os.path.splitext(me)[0] + ".py" + versioneer_file = os.path.relpath(me) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) + present = False + try: + f = open(".gitattributes", "r") + for line in f.readlines(): + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + f.close() + except EnvironmentError: + pass + if not present: + f = open(".gitattributes", "a+") + f.write("%s export-subst\n" % versionfile_source) + f.close() + files.append(".gitattributes") + run_command(GITS, ["add", "--"] + files) + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with " + "prefix '%s'" % (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.15) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +import json +import sys + +version_json = ''' +%s +''' # END VERSION_JSON + + +def get_versions(): + return json.loads(version_json) +""" + + +def versions_from_file(filename): + try: + with open(filename) as f: + contents = f.read() + except EnvironmentError: + raise NotThisMethod("unable to read _version.py") + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + raise NotThisMethod("no version_json in _version.py") + return json.loads(mo.group(1)) + + +def write_to_version_file(filename, versions): + os.unlink(filename) + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) + with open(filename, "w") as f: + f.write(SHORT_VERSION_PY % contents) + + print("set %s to '%s'" % (filename, versions["version"])) + + +def plus_or_dot(pieces): + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + # now build up version string, with post-release "local version + # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + # exceptions: + # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + # TAG[.post.devDISTANCE] . No -dirty + + # exceptions: + # 1: no tags. 0.post.devDISTANCE + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that + # .dev0 sorts backwards (a dirty tree will appear "older" than the + # corresponding clean one), but you shouldn't be releasing software with + # -dirty anyways. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty + # --always' + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty + # --always -long'. The distance/hash is unconditional. + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +class VersioneerBadRootError(Exception): + pass + + +def get_versions(verbose=False): + # returns dict with two keys: 'version' and 'full' + + if "versioneer" in sys.modules: + # see the discussion in cmdclass.py:get_cmdclass() + del sys.modules["versioneer"] + + root = get_root() + cfg = get_config_from_root(root) + + assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" + handlers = HANDLERS.get(cfg.VCS) + assert handlers, "unrecognized VCS '%s'" % cfg.VCS + verbose = verbose or cfg.verbose + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" + assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" + + versionfile_abs = os.path.join(root, cfg.versionfile_source) + + # extract version from first of: _version.py, VCS command (e.g. 'git + # describe'), parentdir. This is meant to work for developers using a + # source checkout, for users of a tarball created by 'setup.py sdist', + # and for users of a tarball/zipball created by 'git archive' or github's + # download-from-tag feature or the equivalent in other VCSes. + + get_keywords_f = handlers.get("get_keywords") + from_keywords_f = handlers.get("keywords") + if get_keywords_f and from_keywords_f: + try: + keywords = get_keywords_f(versionfile_abs) + ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) + if verbose: + print("got version from expanded keyword %s" % ver) + return ver + except NotThisMethod: + pass + + try: + ver = versions_from_file(versionfile_abs) + if verbose: + print("got version from file %s %s" % (versionfile_abs, ver)) + return ver + except NotThisMethod: + pass + + from_vcs_f = handlers.get("pieces_from_vcs") + if from_vcs_f: + try: + pieces = from_vcs_f(cfg.tag_prefix, root, verbose) + ver = render(pieces, cfg.style) + if verbose: + print("got version from VCS %s" % ver) + return ver + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + if verbose: + print("got version from parentdir %s" % ver) + return ver + except NotThisMethod: + pass + + if verbose: + print("unable to compute version") + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version"} + + +def get_version(): + return get_versions()["version"] + + +def get_cmdclass(): + if "versioneer" in sys.modules: + del sys.modules["versioneer"] + # this fixes the "python setup.py develop" case (also 'install' and + # 'easy_install .'), in which subdependencies of the main project are + # built (using setup.py bdist_egg) in the same python process. Assume + # a main project A and a dependency B, which use different versions + # of Versioneer. A's setup.py imports A's Versioneer, leaving it in + # sys.modules by the time B's setup.py is executed, causing B to run + # with the wrong versioneer. Setuptools wraps the sub-dep builds in a + # sandbox that restores sys.modules to it's pre-build state, so the + # parent is protected against the child's "import versioneer". By + # removing ourselves from sys.modules here, before the child build + # happens, we protect the child from the parent's versioneer too. + # Also see https://github.com/warner/python-versioneer/issues/52 + + cmds = {} + + # we add "version" to both distutils and setuptools + from distutils.core import Command + + class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + vers = get_versions(verbose=True) + print("Version: %s" % vers["version"]) + print(" full-revisionid: %s" % vers.get("full-revisionid")) + print(" dirty: %s" % vers.get("dirty")) + if vers["error"]: + print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version + + # we override "build_py" in both distutils and setuptools + # + # most invocation pathways end up running build_py: + # distutils/build -> build_py + # distutils/install -> distutils/build ->.. + # setuptools/bdist_wheel -> distutils/install ->.. + # setuptools/bdist_egg -> distutils/install_lib -> build_py + # setuptools/install -> bdist_egg ->.. + # setuptools/develop -> ? + + from distutils.command.build_py import build_py as _build_py + + class cmd_build_py(_build_py): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_py.run(self) + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if cfg.versionfile_build: + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py + + if "cx_Freeze" in sys.modules: # cx_freeze enabled? + from cx_Freeze.dist import build_exe as _build_exe + + class cmd_build_exe(_build_exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _build_exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["build_exe"] = cmd_build_exe + del cmds["build_py"] + + # we override different "sdist" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.sdist import sdist as _sdist + else: + from distutils.command.sdist import sdist as _sdist + + class cmd_sdist(_sdist): + def run(self): + versions = get_versions() + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old + # version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + root = get_root() + cfg = get_config_from_root(root) + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory + # (remembering that it may be a hardlink) and replace it with an + # updated value + target_versionfile = os.path.join(base_dir, cfg.versionfile_source) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) + cmds["sdist"] = cmd_sdist + + return cmds + + +CONFIG_ERROR = """ +setup.cfg is missing the necessary Versioneer configuration. You need +a section like: + + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = "" + parentdir_prefix = myproject- + +You will also need to edit your setup.py to use the results: + + import versioneer + setup(version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), ...) + +Please read the docstring in ./versioneer.py for configuration instructions, +edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. +""" + +SAMPLE_CONFIG = """ +# See the docstring in versioneer.py for instructions. Note that you must +# re-run 'versioneer.py setup' after changing this section, and commit the +# resulting files. + +[versioneer] +#VCS = git +#style = pep440 +#versionfile_source = +#versionfile_build = +#tag_prefix = +#parentdir_prefix = + +""" + +INIT_PY_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + + +def do_setup(): + root = get_root() + try: + cfg = get_config_from_root(root) + except (EnvironmentError, configparser.NoSectionError, + configparser.NoOptionError) as e: + if isinstance(e, (EnvironmentError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) + with open(os.path.join(root, "setup.cfg"), "a") as f: + f.write(SAMPLE_CONFIG) + print(CONFIG_ERROR, file=sys.stderr) + return 1 + + print(" creating %s" % cfg.versionfile_source) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + if os.path.exists(ipy): + try: + with open(ipy, "r") as f: + old = f.read() + except EnvironmentError: + old = "" + if INIT_PY_SNIPPET not in old: + print(" appending to %s" % ipy) + with open(ipy, "a") as f: + f.write(INIT_PY_SNIPPET) + else: + print(" %s unmodified" % ipy) + else: + print(" %s doesn't exist, ok" % ipy) + ipy = None + + # Make sure both the top-level "versioneer.py" and versionfile_source + # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so + # they'll be copied into source distributions. Pip won't be able to + # install the package without this. + manifest_in = os.path.join(root, "MANIFEST.in") + simple_includes = set() + try: + with open(manifest_in, "r") as f: + for line in f: + if line.startswith("include "): + for include in line.split()[1:]: + simple_includes.add(include) + except EnvironmentError: + pass + # That doesn't cover everything MANIFEST.in can do + # (http://docs.python.org/2/distutils/sourcedist.html#commands), so + # it might give some false negatives. Appending redundant 'include' + # lines is safe, though. + if "versioneer.py" not in simple_includes: + print(" appending 'versioneer.py' to MANIFEST.in") + with open(manifest_in, "a") as f: + f.write("include versioneer.py\n") + else: + print(" 'versioneer.py' already in MANIFEST.in") + if cfg.versionfile_source not in simple_includes: + print(" appending versionfile_source ('%s') to MANIFEST.in" % + cfg.versionfile_source) + with open(manifest_in, "a") as f: + f.write("include %s\n" % cfg.versionfile_source) + else: + print(" versionfile_source already in MANIFEST.in") + + # Make VCS-specific changes. For git, this means creating/changing + # .gitattributes to mark _version.py for export-time keyword + # substitution. + do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + return 0 + + +def scan_setup_py(): + found = set() + setters = False + errors = 0 + with open("setup.py", "r") as f: + for line in f.readlines(): + if "import versioneer" in line: + found.add("import") + if "versioneer.get_cmdclass()" in line: + found.add("cmdclass") + if "versioneer.get_version()" in line: + found.add("get_version") + if "versioneer.VCS" in line: + setters = True + if "versioneer.versionfile_source" in line: + setters = True + if len(found) != 3: + print("") + print("Your setup.py appears to be missing some important items") + print("(but I might be wrong). Please make sure it has something") + print("roughly like the following:") + print("") + print(" import versioneer") + print(" setup( version=versioneer.get_version(),") + print(" cmdclass=versioneer.get_cmdclass(), ...)") + print("") + errors += 1 + if setters: + print("You should remove lines like 'versioneer.VCS = ' and") + print("'versioneer.versionfile_source = ' . This configuration") + print("now lives in setup.cfg, and should be removed from setup.py") + print("") + errors += 1 + return errors + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "setup": + errors = do_setup() + errors += scan_setup_py() + if errors: + sys.exit(1) -- cgit v1.2.3 From aa50bcfddfa16847e4d49d3c6611b0f9bb9fa394 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 18 Nov 2015 19:27:47 -0400 Subject: [feat] bonafide zmq service --- bonafide/README.rst | 2 +- bonafide/src/leap/bonafide/bonafide_cli2 | 112 ++++++++++++++++++++++ bonafide/src/leap/bonafide/config.py | 152 ++++++++++++++++++++++++++++++ bonafide/src/leap/bonafide/cred_srp.py | 9 +- bonafide/src/leap/bonafide/provider.py | 1 + bonafide/src/leap/bonafide/service.py | 120 +++++++++++++++++++++++ bonafide/src/leap/bonafide/zmq_service.py | 97 +++++++++++++++++++ 7 files changed, 487 insertions(+), 6 deletions(-) create mode 100755 bonafide/src/leap/bonafide/bonafide_cli2 create mode 100644 bonafide/src/leap/bonafide/config.py create mode 100644 bonafide/src/leap/bonafide/service.py create mode 100644 bonafide/src/leap/bonafide/zmq_service.py diff --git a/bonafide/README.rst b/bonafide/README.rst index ffcf2ff..044ac71 100644 --- a/bonafide/README.rst +++ b/bonafide/README.rst @@ -17,4 +17,4 @@ the package in development mode by running:: from the parent folder. -Then you can use `bonafide_cli -h` to see the available commands. +Then you can use `bonafide_cli2 -h` to see the available commands. diff --git a/bonafide/src/leap/bonafide/bonafide_cli2 b/bonafide/src/leap/bonafide/bonafide_cli2 new file mode 100755 index 0000000..18506d9 --- /dev/null +++ b/bonafide/src/leap/bonafide/bonafide_cli2 @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# bonafide_cli2.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Bonafide command line interface: zmq client. +""" +import sys +import getpass +import argparse + +from colorama import init as color_init +from colorama import Fore +from twisted.internet import reactor +from txzmq import ZmqEndpoint, ZmqFactory, ZmqREQConnection +import zmq + +from leap.bonafide import config + +description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' + 'using the bonafide protocol. This client connects to ' + 'a running Bonafide service.' + Fore.RESET) + +parser = argparse.ArgumentParser(description=description) +parser.add_argument("--stats", dest="do_stats", action="store_true", + help="print service stats") +parser.add_argument("--signup", action="store_true", dest="do_signup", + help="signup new user") +parser.add_argument("--auth", dest="do_auth", action="store_true", + help="authenticate the passed user") +parser.add_argument("--logout", dest="do_logout", action="store_true", + help="logout this user") +parser.add_argument("--username", dest="username", + help="user to operate with") +parser.add_argument("--shutdown", dest="do_shutdown", action="store_true", + help="shutdown the bonafide service.") +ns = parser.parse_args() + + +def get_zmq_connection(): + zf = ZmqFactory() + e = ZmqEndpoint('connect', config.ENDPOINT) + return ZmqREQConnection(zf, e) + + +def error(msg): + print Fore.RED + "[!] %s" % msg + Fore.RESET + sys.exit(1) + +if len(sys.argv) < 2: + error("Too few arguments. Try %s --help" % sys.argv[0]) + + +if (ns.do_signup or ns.do_auth or ns.do_logout) and not ns.username: + error(Fore.RED + "Need to pass a username for signup/auth/logout" + + Fore.RESET) + +if ns.username and '@' not in ns.username: + error(Fore.RED + "Username must be in the form user@provider" + Fore.RESET) + + +def do_print(stuff): + print Fore.GREEN + stuff[0] + Fore.RESET + + +def send_command(): + + cb = do_print + if ns.do_shutdown: + data = ("shutdown",) + + elif ns.do_stats: + data = ("stats",) + + elif ns.do_signup: + passwd = getpass.getpass() + data = ("signup", ns.username, passwd) + + elif ns.do_auth: + passwd = getpass.getpass() + data = ("authenticate", ns.username, passwd) + + elif ns.do_logout: + passwd = getpass.getpass() + data = ("logout", ns.username, passwd) + + s = get_zmq_connection() + try: + d = s.sendMsg(*data) + except zmq.error.Again: + print Fore.RED + "[ERROR] Server is down :(" + Fore.RESET + d.addCallback(cb) + d.addCallback(lambda x: reactor.stop()) + + +if __name__ == "__main__": + color_init() + reactor.callWhenRunning(reactor.callLater, 0, send_command) + reactor.run() diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py new file mode 100644 index 0000000..9490f55 --- /dev/null +++ b/bonafide/src/leap/bonafide/config.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# config.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Configuration for a LEAP provider. +""" +import datetime +import os +import sys + +from leap.bonafide._http import httpRequest + +from leap.common.check import leap_assert +from leap.common.config import get_path_prefix as common_get_path_prefix +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p + + +APPNAME = "bonafide" +ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME + + +def get_path_prefix(standalone=False): + return common_get_path_prefix(standalone) + + +def get_provider_path(domain): + """ + Returns relative path for provider config. + + :param domain: the domain to which this providerconfig belongs to. + :type domain: str + :returns: the path + :rtype: str + """ + leap_assert(domain is not None, "get_provider_path: We need a domain") + return os.path.join("leap", "providers", domain, "provider.json") + + +def get_modification_ts(path): + """ + Gets modification time of a file. + + :param path: the path to get ts from + :type path: str + :returns: modification time + :rtype: datetime object + """ + ts = os.path.getmtime(path) + return datetime.datetime.fromtimestamp(ts) + + +def update_modification_ts(path): + """ + Sets modification time of a file to current time. + + :param path: the path to set ts to. + :type path: str + :returns: modification time + :rtype: datetime object + """ + os.utime(path, None) + return get_modification_ts(path) + + +def is_file(path): + """ + Returns True if the path exists and is a file. + """ + return os.path.isfile(path) + + +def is_empty_file(path): + """ + Returns True if the file at path is empty. + """ + return os.stat(path).st_size is 0 + + +def make_address(user, provider): + """ + Return a full identifier for an user, as a email-like + identifier. + + :param user: the username + :type user: basestring + :param provider: the provider domain + :type provider: basestring + """ + return "%s@%s" % (user, provider) + + +def get_username_and_provider(full_id): + return full_id.split('@') + + +class ProviderConfig(object): + # TODO add file config for enabled services + + def __init__(self, domain): + self._api_base = None + self._domain = domain + + def is_configured(self): + provider_json = self._get_provider_json_path() + # XXX check if all the services are there + if is_file(provider_json): + return True + return False + + def download_provider_info(self): + """ + Download the provider.json info from the main domain. + This SHOULD only be used once with the DOMAIN url. + """ + # TODO handle pre-seeded providers? + # or let client handle that? We could move them to bonafide. + provider_json = self._get_provider_json_path() + if is_file(provider_json): + raise RuntimeError('File already exists') + + def update_provider_info(self): + """ + Get more recent copy of provider.json from the api URL. + """ + pass + + def _http_request(self, *args, **kw): + # XXX pass if-modified-since header + return httpRequest(*args, **kw) + + def _get_provider_json_path(self): + domain = self._domain.encode(sys.getfilesystemencoding()) + provider_json = os.path.join(get_path_prefix(), get_provider_path(domain)) + return provider_json + +if __name__ == '__main__': + config = ProviderConfig('cdev.bitmask.net') + config.is_configured() + config.download_provider_info() diff --git a/bonafide/src/leap/bonafide/cred_srp.py b/bonafide/src/leap/bonafide/cred_srp.py index 5188830..9fcb97b 100644 --- a/bonafide/src/leap/bonafide/cred_srp.py +++ b/bonafide/src/leap/bonafide/cred_srp.py @@ -31,7 +31,7 @@ from twisted.cred import portal, credentials, error as credError from twisted.cred.checkers import ICredentialsChecker from twisted.internet import defer, reactor -from leap.bonafide.session import LeapSession +from leap.bonafide.session import Session @implementer(ICredentialsChecker) @@ -63,7 +63,7 @@ class SRPCredentialsChecker(object): self._check_srp_auth) def _check_srp_auth(session, username): - if session.authenticated: + if session.is_authenticated: # is ok! --- should add it to some global cache? return defer.succeed(username) else: @@ -72,9 +72,8 @@ class SRPCredentialsChecker(object): def _get_leap_session(credentials): - session = LeapSession(credentials) - d = session.handshake() - d.addCallback(lambda _: session.authenticate()) + session = Session(credentials) + d = session.authenticate() d.addCallback(lambda _: session) return d diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py index c22be3c..1d0b5b7 100644 --- a/bonafide/src/leap/bonafide/provider.py +++ b/bonafide/src/leap/bonafide/provider.py @@ -93,6 +93,7 @@ class Api(object): # TODO when should the provider-api object be created? # TODO pass a Provider object to constructor, with autoconf flag. # TODO make the actions attribute depend on the api version + # TODO missing UPDATE USER RECORD __metaclass__ = _MetaActionDispatcher _actions = { diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py new file mode 100644 index 0000000..1f7295a --- /dev/null +++ b/bonafide/src/leap/bonafide/service.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Bonafide service. +""" +import os +import resource +from collections import defaultdict + +from leap.bonafide import config +from leap.bonafide import provider +from leap.bonafide.session import Session, OK + +from twisted.cred.credentials import UsernamePassword +from twisted.internet.defer import fail +from twisted.python import log + + +# TODO [ ] enable-disable services +# TODO [ ] read provider info + +COMMANDS = 'signup', 'authenticate', 'logout', 'stats' + + +class BonafideService(object): + """ + Expose the Bonafide Service API. + """ + + _apis = defaultdict(None) + _sessions = defaultdict(None) + + def _get_api(self, provider_id): + if provider_id in self._apis: + return self._apis[provider_id] + + # XXX lookup the provider config instead + # TODO defer the autoconfig for the provider if needed... + api = provider.Api('https://api.%s:4430' % provider_id) + self._apis[provider_id] = api + return api + + def _get_session(self, full_id, password=""): + if full_id in self._sessions: + return self._sessions[full_id] + + # TODO if password/username null, then pass AnonymousCreds + # TODO use twisted.cred instead + username, provider_id = config.get_username_and_provider(full_id) + credentials = UsernamePassword(username, password) + api = self._get_api(provider_id) + cdev_pem = os.path.expanduser( + '~/.config/leap/providers/%s/keys/ca/cacert.pem' % + provider_id) + session = Session(credentials, api, cdev_pem) + self._sessions[full_id] = session + return session + + # Service public methods + + def do_signup(self, full_id, password): + # XXX check it's unauthenticated + def return_user(result, _session): + return_code, user = result + if return_code == OK: + return user + + log.msg('SIGNUP for %s' % full_id) + session = self._get_session(full_id, password) + username, provider_id = config.get_username_and_provider(full_id) + + d = session.signup(username, password) + d.addCallback(return_user, session) + return d + + def do_authenticate(self, full_id, password): + def return_token(result, _session): + if result == OK: + return str(_session.token) + + log.msg('AUTH for %s' % full_id) + session = self._get_session(full_id, password) + d = session.authenticate() + d.addCallback(return_token, session) + return d + + def do_logout(self, full_id, password): + # XXX use the AVATAR here + log.msg('LOGOUT for %s' % full_id) + session = self._get_session(full_id) + if not session.is_authenticated: + return fail(RuntimeError("There is no session for such user")) + try: + d = session.logout() + except Exception as exc: + log.err(exc) + return fail(exc) + + d.addCallback(lambda _: self._sessions.pop(full_id)) + d.addCallback(lambda _: '%s logged out' % full_id) + return d + + def do_stats(self): + mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( + len(self._sessions), mem / 1024) diff --git a/bonafide/src/leap/bonafide/zmq_service.py b/bonafide/src/leap/bonafide/zmq_service.py new file mode 100644 index 0000000..e33df3d --- /dev/null +++ b/bonafide/src/leap/bonafide/zmq_service.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# zmq_service.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Bonafide ZMQ Service +""" +from leap.bonafide import config +from leap.bonafide.service import BonafideService, COMMANDS + +from txzmq import ZmqEndpoint, ZmqFactory, ZmqREPConnection + +from twisted.python import log + + +class BonafideZmqREPConnection(ZmqREPConnection): + + def initialize(self): + self._service = BonafideService() + + def do_greet(self): + print "[+] Bonafide service running..." + + def do_bye(self): + print "[+] Bonafide service stopped. Have a nice day." + reactor.stop() + + def gotMessage(self, msgId, *parts): + def defer_reply(response): + reactor.callLater(0, self.reply, msgId, str(response)) + + def log_err(failure): + log.err(failure) + print "FAILURE", failure + defer_reply("ERROR: %r" % failure) + + cmd = parts[0] + + if cmd == "shutdown": + defer_reply('ok, shutting down') + reactor.callLater(1, self.do_bye) + + if cmd not in COMMANDS: + response = 'INVALID COMMAND' + defer_reply(response) + + elif cmd == 'signup': + username, password = parts[1], parts[2] + d = self._service.do_signup(username, password) + d.addCallback(lambda response: defer_reply( + 'REGISTERED -> %s' % response)) + d.addErrback(log_err) + + elif cmd == 'authenticate': + username, password = parts[1], parts[2] + d = self._service.do_authenticate(username, password) + d.addCallback(lambda response: defer_reply( + 'TOKEN -> %s' % response)) + d.addErrback(log_err) + + elif cmd == 'logout': + username, password = parts[1], parts[2] + d = self._service.do_logout(username, password) + d.addCallback(lambda response: defer_reply( + 'LOGOUT -> ok')) + d.addErrback(log_err) + + elif cmd == 'stats': + response = self._service.do_stats() + defer_reply(response) + + +def get_zmq_connection(): + zf = ZmqFactory() + e = ZmqEndpoint("bind", config.ENDPOINT) + return BonafideZmqREPConnection(zf, e) + + +if __name__ == "__main__": + from twisted.internet import reactor + + s = get_zmq_connection() + reactor.callWhenRunning(s.initialize) + reactor.callWhenRunning(s.do_greet) + reactor.run() -- cgit v1.2.3 From 7153d5ef69f7a11d83e0c689689d6c687cbd03e7 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 18 Nov 2015 19:57:45 -0400 Subject: add stubs for missing functionality --- bonafide/src/leap/bonafide/service.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 1f7295a..24004fa 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -118,3 +118,12 @@ class BonafideService(object): mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( len(self._sessions), mem / 1024) + + def do_get_vpn_cert(self): + pass + + def do_get_smtp_cert(self): + pass + + def do_update_user(self): + pass -- cgit v1.2.3 From 4cfd0c8a929679a0a468f53f4ad5dcd2c83670fe Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 19 Nov 2015 00:01:08 -0400 Subject: [feat] use twisted IService interface to run bonafide service --- bonafide/.gitignore | 1 + bonafide/Makefile | 2 + bonafide/README.rst | 4 + bonafide/src/leap/bonafide/_protocol.py | 130 ++++++++++++++++++++++++++++++ bonafide/src/leap/bonafide/service.py | 129 ----------------------------- bonafide/src/leap/bonafide/zmq.tac | 10 +++ bonafide/src/leap/bonafide/zmq_service.py | 51 ++++++------ 7 files changed, 173 insertions(+), 154 deletions(-) create mode 100644 bonafide/Makefile create mode 100644 bonafide/src/leap/bonafide/_protocol.py delete mode 100644 bonafide/src/leap/bonafide/service.py create mode 100644 bonafide/src/leap/bonafide/zmq.tac diff --git a/bonafide/.gitignore b/bonafide/.gitignore index 9ac4b93..6fbbf52 100644 --- a/bonafide/.gitignore +++ b/bonafide/.gitignore @@ -4,6 +4,7 @@ *.swp *.swo .project +*.pid dist/ build/ MANIFEST diff --git a/bonafide/Makefile b/bonafide/Makefile new file mode 100644 index 0000000..c311005 --- /dev/null +++ b/bonafide/Makefile @@ -0,0 +1,2 @@ +bonafide_server: + twistd -n -y src/leap/bonafide/zmq.tac diff --git a/bonafide/README.rst b/bonafide/README.rst index 044ac71..1a4e44e 100644 --- a/bonafide/README.rst +++ b/bonafide/README.rst @@ -17,4 +17,8 @@ the package in development mode by running:: from the parent folder. +To run the bonafide daemon:: + + make bonafide_server + Then you can use `bonafide_cli2 -h` to see the available commands. diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py new file mode 100644 index 0000000..b093b9c --- /dev/null +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# _protocol.py +# Copyright (C) 2014-2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Bonafide protocol. +""" +import os +import resource +from collections import defaultdict + +from leap.bonafide import config +from leap.bonafide import provider +from leap.bonafide.session import Session, OK + +from twisted.cred.credentials import UsernamePassword +from twisted.internet.defer import fail +from twisted.python import log + + +# TODO [ ] enable-disable services +# TODO [ ] read provider info + +COMMANDS = 'signup', 'authenticate', 'logout', 'stats' + + +class BonafideProtocol(object): + """ + Expose the protocol that interacts with the Bonafide Service API. + """ + + _apis = defaultdict(None) + _sessions = defaultdict(None) + + def _get_api(self, provider_id): + if provider_id in self._apis: + return self._apis[provider_id] + + # XXX lookup the provider config instead + # TODO defer the autoconfig for the provider if needed... + api = provider.Api('https://api.%s:4430' % provider_id) + self._apis[provider_id] = api + return api + + def _get_session(self, full_id, password=""): + if full_id in self._sessions: + return self._sessions[full_id] + + # TODO if password/username null, then pass AnonymousCreds + # TODO use twisted.cred instead + username, provider_id = config.get_username_and_provider(full_id) + credentials = UsernamePassword(username, password) + api = self._get_api(provider_id) + cdev_pem = os.path.expanduser( + '~/.config/leap/providers/%s/keys/ca/cacert.pem' % + provider_id) + session = Session(credentials, api, cdev_pem) + self._sessions[full_id] = session + return session + + # Service public methods + + def do_signup(self, full_id, password): + # XXX check it's unauthenticated + def return_user(result, _session): + return_code, user = result + if return_code == OK: + return user + + log.msg('SIGNUP for %s' % full_id) + session = self._get_session(full_id, password) + username, provider_id = config.get_username_and_provider(full_id) + + d = session.signup(username, password) + d.addCallback(return_user, session) + return d + + def do_authenticate(self, full_id, password): + def return_token(result, _session): + if result == OK: + return str(_session.token) + + log.msg('AUTH for %s' % full_id) + session = self._get_session(full_id, password) + d = session.authenticate() + d.addCallback(return_token, session) + return d + + def do_logout(self, full_id, password): + # XXX use the AVATAR here + log.msg('LOGOUT for %s' % full_id) + session = self._get_session(full_id) + if not session.is_authenticated: + return fail(RuntimeError("There is no session for such user")) + try: + d = session.logout() + except Exception as exc: + log.err(exc) + return fail(exc) + + d.addCallback(lambda _: self._sessions.pop(full_id)) + d.addCallback(lambda _: '%s logged out' % full_id) + return d + + def do_stats(self): + log.msg('Calculating Bonafide STATS') + mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( + len(self._sessions), mem / 1024) + + def do_get_vpn_cert(self): + pass + + def do_get_smtp_cert(self): + pass + + def do_update_user(self): + pass diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py deleted file mode 100644 index 24004fa..0000000 --- a/bonafide/src/leap/bonafide/service.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -# service.py -# Copyright (C) 2014 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Bonafide service. -""" -import os -import resource -from collections import defaultdict - -from leap.bonafide import config -from leap.bonafide import provider -from leap.bonafide.session import Session, OK - -from twisted.cred.credentials import UsernamePassword -from twisted.internet.defer import fail -from twisted.python import log - - -# TODO [ ] enable-disable services -# TODO [ ] read provider info - -COMMANDS = 'signup', 'authenticate', 'logout', 'stats' - - -class BonafideService(object): - """ - Expose the Bonafide Service API. - """ - - _apis = defaultdict(None) - _sessions = defaultdict(None) - - def _get_api(self, provider_id): - if provider_id in self._apis: - return self._apis[provider_id] - - # XXX lookup the provider config instead - # TODO defer the autoconfig for the provider if needed... - api = provider.Api('https://api.%s:4430' % provider_id) - self._apis[provider_id] = api - return api - - def _get_session(self, full_id, password=""): - if full_id in self._sessions: - return self._sessions[full_id] - - # TODO if password/username null, then pass AnonymousCreds - # TODO use twisted.cred instead - username, provider_id = config.get_username_and_provider(full_id) - credentials = UsernamePassword(username, password) - api = self._get_api(provider_id) - cdev_pem = os.path.expanduser( - '~/.config/leap/providers/%s/keys/ca/cacert.pem' % - provider_id) - session = Session(credentials, api, cdev_pem) - self._sessions[full_id] = session - return session - - # Service public methods - - def do_signup(self, full_id, password): - # XXX check it's unauthenticated - def return_user(result, _session): - return_code, user = result - if return_code == OK: - return user - - log.msg('SIGNUP for %s' % full_id) - session = self._get_session(full_id, password) - username, provider_id = config.get_username_and_provider(full_id) - - d = session.signup(username, password) - d.addCallback(return_user, session) - return d - - def do_authenticate(self, full_id, password): - def return_token(result, _session): - if result == OK: - return str(_session.token) - - log.msg('AUTH for %s' % full_id) - session = self._get_session(full_id, password) - d = session.authenticate() - d.addCallback(return_token, session) - return d - - def do_logout(self, full_id, password): - # XXX use the AVATAR here - log.msg('LOGOUT for %s' % full_id) - session = self._get_session(full_id) - if not session.is_authenticated: - return fail(RuntimeError("There is no session for such user")) - try: - d = session.logout() - except Exception as exc: - log.err(exc) - return fail(exc) - - d.addCallback(lambda _: self._sessions.pop(full_id)) - d.addCallback(lambda _: '%s logged out' % full_id) - return d - - def do_stats(self): - mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss - return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( - len(self._sessions), mem / 1024) - - def do_get_vpn_cert(self): - pass - - def do_get_smtp_cert(self): - pass - - def do_update_user(self): - pass diff --git a/bonafide/src/leap/bonafide/zmq.tac b/bonafide/src/leap/bonafide/zmq.tac new file mode 100644 index 0000000..d3a9625 --- /dev/null +++ b/bonafide/src/leap/bonafide/zmq.tac @@ -0,0 +1,10 @@ +# Run as: twistd -n -y zmq.tac +from twisted.application import service +from leap.bonafide.zmq_service import BonafideZMQService + +top_service = service.MultiService() +bonafide_zmq_service = BonafideZMQService() +bonafide_zmq_service.setServiceParent(top_service) + +application = service.Application("bonafide") +top_service.setServiceParent(application) diff --git a/bonafide/src/leap/bonafide/zmq_service.py b/bonafide/src/leap/bonafide/zmq_service.py index e33df3d..df8e234 100644 --- a/bonafide/src/leap/bonafide/zmq_service.py +++ b/bonafide/src/leap/bonafide/zmq_service.py @@ -15,23 +15,39 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -Bonafide ZMQ Service +Bonafide ZMQ Service. """ from leap.bonafide import config -from leap.bonafide.service import BonafideService, COMMANDS +from leap.bonafide.protocol import BonafideProtocol, COMMANDS from txzmq import ZmqEndpoint, ZmqFactory, ZmqREPConnection +from twisted.application import service +from twisted.internet import reactor from twisted.python import log -class BonafideZmqREPConnection(ZmqREPConnection): +class BonafideZMQService(service.Service): - def initialize(self): - self._service = BonafideService() + def __init__(self): + self._bonafide = BonafideProtocol() + self._conn = None + + def startService(self): + zf = ZmqFactory() + e = ZmqEndpoint("bind", config.ENDPOINT) + self._conn = _BonafideZmqREPConnection(zf, e, self._bonafide) + reactor.callWhenRunning(self._conn.do_greet) + + +class _BonafideZmqREPConnection(ZmqREPConnection): + + def __init__(self, zf, e, bonafide): + ZmqREPConnection.__init__(self, zf, e) + self._bonafide = bonafide def do_greet(self): - print "[+] Bonafide service running..." + print "Bonafide service running..." def do_bye(self): print "[+] Bonafide service stopped. Have a nice day." @@ -58,40 +74,25 @@ class BonafideZmqREPConnection(ZmqREPConnection): elif cmd == 'signup': username, password = parts[1], parts[2] - d = self._service.do_signup(username, password) + d = self._bonafide.do_signup(username, password) d.addCallback(lambda response: defer_reply( 'REGISTERED -> %s' % response)) d.addErrback(log_err) elif cmd == 'authenticate': username, password = parts[1], parts[2] - d = self._service.do_authenticate(username, password) + d = self._bonafide.do_authenticate(username, password) d.addCallback(lambda response: defer_reply( 'TOKEN -> %s' % response)) d.addErrback(log_err) elif cmd == 'logout': username, password = parts[1], parts[2] - d = self._service.do_logout(username, password) + d = self._bonafide.do_logout(username, password) d.addCallback(lambda response: defer_reply( 'LOGOUT -> ok')) d.addErrback(log_err) elif cmd == 'stats': - response = self._service.do_stats() + response = self._bonafide.do_stats() defer_reply(response) - - -def get_zmq_connection(): - zf = ZmqFactory() - e = ZmqEndpoint("bind", config.ENDPOINT) - return BonafideZmqREPConnection(zf, e) - - -if __name__ == "__main__": - from twisted.internet import reactor - - s = get_zmq_connection() - reactor.callWhenRunning(s.initialize) - reactor.callWhenRunning(s.do_greet) - reactor.run() -- cgit v1.2.3 From 34e429b4db5c3563f4a83600e5dfcb5125937db5 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 19 Nov 2015 00:03:33 -0400 Subject: documentation cleanup --- bonafide/README.rst | 3 ++- bonafide/src/leap/bonafide/_protocol.py | 2 +- bonafide/src/leap/bonafide/_srp.py | 2 +- bonafide/src/leap/bonafide/ssh_service.py | 2 ++ bonafide/src/leap/bonafide/ws_service.py | 1 + bonafide/src/leap/bonafide/zmq_service.py | 14 +++++++++++--- 6 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 bonafide/src/leap/bonafide/ssh_service.py create mode 100644 bonafide/src/leap/bonafide/ws_service.py diff --git a/bonafide/README.rst b/bonafide/README.rst index 1a4e44e..2e10764 100644 --- a/bonafide/README.rst +++ b/bonafide/README.rst @@ -3,7 +3,8 @@ Bonafide Bonafide is the protocol for secure user registration, authentication, and provider discovery for the LEAP applications. See the `Bonafide`_ design docs. -This is a client implementation, written in python. +This is a client implementation, written in python. It consists of a python library, a +twisted service and a command-line interface to interact locally with it. .. _`Bonafide`: https://leap.se/en/docs/design/bonafide diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index b093b9c..6a1c3d2 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -115,7 +115,7 @@ class BonafideProtocol(object): return d def do_stats(self): - log.msg('Calculating Bonafide STATS') + log.msg('Calculating Bonafide Service STATS') mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( len(self._sessions), mem / 1024) diff --git a/bonafide/src/leap/bonafide/_srp.py b/bonafide/src/leap/bonafide/_srp.py index 4f4a605..1c711f3 100644 --- a/bonafide/src/leap/bonafide/_srp.py +++ b/bonafide/src/leap/bonafide/_srp.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# srp_auth.py +# _srp.py # Copyright (C) 2015 LEAP # # This program is free software: you can redistribute it and/or modify diff --git a/bonafide/src/leap/bonafide/ssh_service.py b/bonafide/src/leap/bonafide/ssh_service.py new file mode 100644 index 0000000..3777b10 --- /dev/null +++ b/bonafide/src/leap/bonafide/ssh_service.py @@ -0,0 +1,2 @@ +# TODO expose bonafide protocol through ssh, accessible through a hidden +# service diff --git a/bonafide/src/leap/bonafide/ws_service.py b/bonafide/src/leap/bonafide/ws_service.py new file mode 100644 index 0000000..feaa4b0 --- /dev/null +++ b/bonafide/src/leap/bonafide/ws_service.py @@ -0,0 +1 @@ +# TODO expose bonafide protocol through websockets diff --git a/bonafide/src/leap/bonafide/zmq_service.py b/bonafide/src/leap/bonafide/zmq_service.py index df8e234..38c0305 100644 --- a/bonafide/src/leap/bonafide/zmq_service.py +++ b/bonafide/src/leap/bonafide/zmq_service.py @@ -14,11 +14,13 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + """ Bonafide ZMQ Service. """ + from leap.bonafide import config -from leap.bonafide.protocol import BonafideProtocol, COMMANDS +from leap.bonafide._protocol import BonafideProtocol, COMMANDS from txzmq import ZmqEndpoint, ZmqFactory, ZmqREPConnection @@ -27,6 +29,8 @@ from twisted.internet import reactor from twisted.python import log +# TODO [] should shutdown all the ongoing connections when stopping the service + class BonafideZMQService(service.Service): def __init__(self): @@ -36,9 +40,14 @@ class BonafideZMQService(service.Service): def startService(self): zf = ZmqFactory() e = ZmqEndpoint("bind", config.ENDPOINT) + self._conn = _BonafideZmqREPConnection(zf, e, self._bonafide) reactor.callWhenRunning(self._conn.do_greet) + #def stopService(self): + # pass + + class _BonafideZmqREPConnection(ZmqREPConnection): @@ -50,7 +59,7 @@ class _BonafideZmqREPConnection(ZmqREPConnection): print "Bonafide service running..." def do_bye(self): - print "[+] Bonafide service stopped. Have a nice day." + print "Bonafide service stopped. Have a nice day." reactor.stop() def gotMessage(self, msgId, *parts): @@ -59,7 +68,6 @@ class _BonafideZmqREPConnection(ZmqREPConnection): def log_err(failure): log.err(failure) - print "FAILURE", failure defer_reply("ERROR: %r" % failure) cmd = parts[0] -- cgit v1.2.3 From 4a974bd742939ec75e2004a22e316bffa286658a Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 19 Nov 2015 17:18:16 -0400 Subject: add leap.common dependency --- bonafide/pkg/requirements-leap.pip | 1 + 1 file changed, 1 insertion(+) create mode 100644 bonafide/pkg/requirements-leap.pip diff --git a/bonafide/pkg/requirements-leap.pip b/bonafide/pkg/requirements-leap.pip new file mode 100644 index 0000000..72d41e4 --- /dev/null +++ b/bonafide/pkg/requirements-leap.pip @@ -0,0 +1 @@ +leap.common -- cgit v1.2.3 From 3a90c55b82034cada54613c46b5fc491085180a9 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 20 Nov 2015 01:16:32 -0400 Subject: service hooks [WIP] --- bonafide/src/leap/bonafide/bonafide_cli2 | 10 ++++++- bonafide/src/leap/bonafide/zmq.tac | 8 ++++++ bonafide/src/leap/bonafide/zmq_service.py | 48 +++++++++++++++++++++++++++---- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/bonafide/src/leap/bonafide/bonafide_cli2 b/bonafide/src/leap/bonafide/bonafide_cli2 index 18506d9..bf05a99 100755 --- a/bonafide/src/leap/bonafide/bonafide_cli2 +++ b/bonafide/src/leap/bonafide/bonafide_cli2 @@ -47,6 +47,11 @@ parser.add_argument("--username", dest="username", help="user to operate with") parser.add_argument("--shutdown", dest="do_shutdown", action="store_true", help="shutdown the bonafide service.") + +# XXX DEBUG -------------------------------------------------------- +parser.add_argument("--debug", dest="do_debug", action="store_true", + help="debug command, can be anything") +# ------------------------------------------------------------------ ns = parser.parse_args() @@ -97,11 +102,14 @@ def send_command(): passwd = getpass.getpass() data = ("logout", ns.username, passwd) + elif ns.do_debug: + data = ("get_soledad",) + s = get_zmq_connection() try: d = s.sendMsg(*data) except zmq.error.Again: - print Fore.RED + "[ERROR] Server is down :(" + Fore.RESET + print Fore.RED + "[ERROR] Server is down" + Fore.RESET d.addCallback(cb) d.addCallback(lambda x: reactor.stop()) diff --git a/bonafide/src/leap/bonafide/zmq.tac b/bonafide/src/leap/bonafide/zmq.tac index d3a9625..f429ff5 100644 --- a/bonafide/src/leap/bonafide/zmq.tac +++ b/bonafide/src/leap/bonafide/zmq.tac @@ -1,10 +1,18 @@ # Run as: twistd -n -y zmq.tac from twisted.application import service from leap.bonafide.zmq_service import BonafideZMQService +from leap.bonafide.soledad_service import SoledadService top_service = service.MultiService() bonafide_zmq_service = BonafideZMQService() bonafide_zmq_service.setServiceParent(top_service) +# XXX DEBUG ------------------------------------- +# This SHOULD BE moved to BITMASK-CORE. +soledad_service = SoledadService() +soledad_service.setName("soledad") +soledad_service.setServiceParent(top_service) +#------------------------------------------------ + application = service.Application("bonafide") top_service.setServiceParent(application) diff --git a/bonafide/src/leap/bonafide/zmq_service.py b/bonafide/src/leap/bonafide/zmq_service.py index 38c0305..59ef47b 100644 --- a/bonafide/src/leap/bonafide/zmq_service.py +++ b/bonafide/src/leap/bonafide/zmq_service.py @@ -37,26 +37,44 @@ class BonafideZMQService(service.Service): self._bonafide = BonafideProtocol() self._conn = None + self.service_hooks = {} + def startService(self): zf = ZmqFactory() e = ZmqEndpoint("bind", config.ENDPOINT) - self._conn = _BonafideZmqREPConnection(zf, e, self._bonafide) + self._conn = _BonafideZmqREPConnection(zf, e, self._bonafide, self) reactor.callWhenRunning(self._conn.do_greet) - #def stopService(self): - # pass + def register_hook(self, kind, service): + print "REGISTERING HOOK", kind, service + self.service_hooks[kind] = service + + + # def stopService(self): + # pass class _BonafideZmqREPConnection(ZmqREPConnection): - def __init__(self, zf, e, bonafide): + def __init__(self, zf, e, bonafide, service): + # XXX passing a ref to the service, + # to be able to access sibling services ZmqREPConnection.__init__(self, zf, e) self._bonafide = bonafide + self._service = service + + def get_sibling_service(self, kind): + return self._service.parent.getServiceNamed(kind) + + def get_hooked_service(self, kind): + hooks = self._service.service_hooks + if kind in hooks: + return self.get_sibling_service(hooks[kind]) def do_greet(self): - print "Bonafide service running..." + print "Starging Bonafide service" def do_bye(self): print "Bonafide service stopped. Have a nice day." @@ -72,11 +90,13 @@ class _BonafideZmqREPConnection(ZmqREPConnection): cmd = parts[0] + # TODO split using dispatcher pattern + if cmd == "shutdown": defer_reply('ok, shutting down') reactor.callLater(1, self.do_bye) - if cmd not in COMMANDS: + if cmd not in COMMANDS + ("get_soledad",): response = 'INVALID COMMAND' defer_reply(response) @@ -88,8 +108,18 @@ class _BonafideZmqREPConnection(ZmqREPConnection): d.addErrback(log_err) elif cmd == 'authenticate': + + def activate_hook(token): + hook_service = self.get_hooked_service('on_auth') + if hook_service: + hook_service.activate_hook( + # TODO GET UUID TOO!! + 'on_auth', username=username, uuid=uuid, token=token) + return token + username, password = parts[1], parts[2] d = self._bonafide.do_authenticate(username, password) + d.addCallback(activate_hook) d.addCallback(lambda response: defer_reply( 'TOKEN -> %s' % response)) d.addErrback(log_err) @@ -104,3 +134,9 @@ class _BonafideZmqREPConnection(ZmqREPConnection): elif cmd == 'stats': response = self._bonafide.do_stats() defer_reply(response) + + # XXX DEBUG --------------------------------------------------------- + elif cmd == 'get_soledad': + response = str(self._service.parent.getServiceNamed("soledad")) + defer_reply(response) + # ------------------------------------------------------------------ -- cgit v1.2.3 From 25e255a55b63ca2ab11526b8e4002a489e3c7ed9 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 24 Nov 2015 23:27:29 -0400 Subject: return token and uuid --- bonafide/src/leap/bonafide/_protocol.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index 6a1c3d2..617c93c 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -88,14 +88,14 @@ class BonafideProtocol(object): return d def do_authenticate(self, full_id, password): - def return_token(result, _session): + def return_token_and_uuid(result, _session): if result == OK: - return str(_session.token) + return str(_session.token), str(_session.uuid) log.msg('AUTH for %s' % full_id) session = self._get_session(full_id, password) d = session.authenticate() - d.addCallback(return_token, session) + d.addCallback(return_token_and_uuid, session) return d def do_logout(self, full_id, password): -- cgit v1.2.3 From 96821d0feebaf1eaef6a10ff4da26153afcb4d17 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 24 Nov 2015 23:27:52 -0400 Subject: naming changes --- bonafide/src/leap/bonafide/zmq_service.py | 35 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/bonafide/src/leap/bonafide/zmq_service.py b/bonafide/src/leap/bonafide/zmq_service.py index 59ef47b..ee9ad6d 100644 --- a/bonafide/src/leap/bonafide/zmq_service.py +++ b/bonafide/src/leap/bonafide/zmq_service.py @@ -29,8 +29,6 @@ from twisted.internet import reactor from twisted.python import log -# TODO [] should shutdown all the ongoing connections when stopping the service - class BonafideZMQService(service.Service): def __init__(self): @@ -45,14 +43,13 @@ class BonafideZMQService(service.Service): self._conn = _BonafideZmqREPConnection(zf, e, self._bonafide, self) reactor.callWhenRunning(self._conn.do_greet) + super(BonafideZMQService, self).startService() - def register_hook(self, kind, service): - print "REGISTERING HOOK", kind, service - self.service_hooks[kind] = service - + def stopService(self): + super(BonafideZMQService, self).stopService() - # def stopService(self): - # pass + def register_hook(self, kind, trigger): + self.service_hooks[kind] = trigger @@ -74,7 +71,7 @@ class _BonafideZmqREPConnection(ZmqREPConnection): return self.get_sibling_service(hooks[kind]) def do_greet(self): - print "Starging Bonafide service" + print "Starting Bonafide service" def do_bye(self): print "Bonafide service stopped. Have a nice day." @@ -109,19 +106,21 @@ class _BonafideZmqREPConnection(ZmqREPConnection): elif cmd == 'authenticate': - def activate_hook(token): - hook_service = self.get_hooked_service('on_auth') - if hook_service: - hook_service.activate_hook( - # TODO GET UUID TOO!! - 'on_auth', username=username, uuid=uuid, token=token) - return token + def notify_hook(result): + this_hook = 'on_bonafide_auth' + token, uuid = result + hooked_service = self.get_hooked_service(this_hook) + if hooked_service: + hooked_service.notify_hook( + this_hook, + username=username, uuid=uuid, token=token) + return result username, password = parts[1], parts[2] d = self._bonafide.do_authenticate(username, password) - d.addCallback(activate_hook) + d.addCallback(notify_hook) d.addCallback(lambda response: defer_reply( - 'TOKEN -> %s' % response)) + 'TOKEN, UUID: %s' % str(response))) d.addErrback(log_err) elif cmd == 'logout': -- cgit v1.2.3 From b611ab8b9e46f962eb0326d027a668652bc228cd Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 2 Dec 2015 02:30:11 -0400 Subject: [refactor] dispatcher methods --- bonafide/src/leap/bonafide/zmq_service.py | 117 ++++++++++++++++++------------ 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/bonafide/src/leap/bonafide/zmq_service.py b/bonafide/src/leap/bonafide/zmq_service.py index ee9ad6d..f2cf123 100644 --- a/bonafide/src/leap/bonafide/zmq_service.py +++ b/bonafide/src/leap/bonafide/zmq_service.py @@ -77,65 +77,90 @@ class _BonafideZmqREPConnection(ZmqREPConnection): print "Bonafide service stopped. Have a nice day." reactor.stop() - def gotMessage(self, msgId, *parts): - def defer_reply(response): - reactor.callLater(0, self.reply, msgId, str(response)) + def defer_reply(self, response, msgId): + reactor.callLater(0, self.reply, msgId, str(response)) - def log_err(failure): - log.err(failure) - defer_reply("ERROR: %r" % failure) + def log_err(self, failure, msgId): + log.err(failure) + self.defer_reply("ERROR: %r" % failure, msgId) - cmd = parts[0] + def gotMessage(self, msgId, *parts): - # TODO split using dispatcher pattern + cmd = parts[0] if cmd == "shutdown": - defer_reply('ok, shutting down') - reactor.callLater(1, self.do_bye) + self.do_shutdown(msgId) if cmd not in COMMANDS + ("get_soledad",): response = 'INVALID COMMAND' - defer_reply(response) + self.defer_reply(response, msgId) elif cmd == 'signup': - username, password = parts[1], parts[2] - d = self._bonafide.do_signup(username, password) - d.addCallback(lambda response: defer_reply( - 'REGISTERED -> %s' % response)) - d.addErrback(log_err) + self.do_signup(parts, msgId) elif cmd == 'authenticate': - - def notify_hook(result): - this_hook = 'on_bonafide_auth' - token, uuid = result - hooked_service = self.get_hooked_service(this_hook) - if hooked_service: - hooked_service.notify_hook( - this_hook, - username=username, uuid=uuid, token=token) - return result - - username, password = parts[1], parts[2] - d = self._bonafide.do_authenticate(username, password) - d.addCallback(notify_hook) - d.addCallback(lambda response: defer_reply( - 'TOKEN, UUID: %s' % str(response))) - d.addErrback(log_err) + self.do_authenticate(parts, msgId) elif cmd == 'logout': - username, password = parts[1], parts[2] - d = self._bonafide.do_logout(username, password) - d.addCallback(lambda response: defer_reply( - 'LOGOUT -> ok')) - d.addErrback(log_err) + self.do_logout(self, parts, msgId) elif cmd == 'stats': - response = self._bonafide.do_stats() - defer_reply(response) - - # XXX DEBUG --------------------------------------------------------- - elif cmd == 'get_soledad': - response = str(self._service.parent.getServiceNamed("soledad")) - defer_reply(response) - # ------------------------------------------------------------------ + self.do_stats(msgId) + + def do_shutdown(self, msgId): + self.defer_reply('ok, shutting down', msgId) + reactor.callLater(1, self.do_bye) + + def do_signup(self, parts, msgId): + username, password = parts[1], parts[2] + d = self._bonafide.do_signup(username, password) + d.addCallback(lambda response: self.defer_reply( + 'REGISTERED -> %s' % response), msgId) + d.addErrback(self.log_err, msgId) + + def do_authenticate(self, parts, msgId): + + username, password = parts[1], parts[2] + + def notify_passphrase_entry(username, password): + this_hook = 'on_passphrase_entry' + hooked_service = self.get_hooked_service(this_hook) + if hooked_service: + hooked_service.notify_hook( + this_hook, username=username, password=password) + + def notify_bonafide_auth_hook(result): + this_hook = 'on_bonafide_auth' + token, uuid = result + hooked_service = self.get_hooked_service(this_hook) + if hooked_service: + hooked_service.notify_hook( + this_hook, + username=username, token=token, uuid=uuid, + password=password) + return result + + # XXX I still have doubts from where it's best to trigger this. + # We probably should wait for BOTH deferreds and + # handle local and remote authentication success together + # (and fail if either one fails). Going with fire-and-forget for + # now, but needs needs improvement. + + notify_passphrase_entry(username, password) + + d = self._bonafide.do_authenticate(username, password) + d.addCallback(notify_bonafide_auth_hook) + d.addCallback(lambda response: self.defer_reply( + 'TOKEN, UUID: %s' % str(response), msgId)) + d.addErrback(self.log_err, msgId) + + def do_logout(self, parts, msgId): + username, password = parts[1], parts[2] + d = self._bonafide.do_logout(username, password) + d.addCallback(lambda response: self.defer_reply( + 'LOGOUT -> ok'), msgId) + d.addErrback(self.log_err, msgId) + + def do_stats(self, msgId): + response = self._bonafide.do_stats() + self.defer_reply(response, msgId) -- cgit v1.2.3 From 6540309e0604098068c004a413652f826af9c1ce Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 3 Dec 2015 23:01:24 -0400 Subject: zmq dispatcher moved to bitmask.core --- bonafide/src/leap/bonafide/service.py | 89 ++++++++++++++++ bonafide/src/leap/bonafide/zmq_service.py | 166 ------------------------------ 2 files changed, 89 insertions(+), 166 deletions(-) create mode 100644 bonafide/src/leap/bonafide/service.py delete mode 100644 bonafide/src/leap/bonafide/zmq_service.py diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py new file mode 100644 index 0000000..b681a58 --- /dev/null +++ b/bonafide/src/leap/bonafide/service.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2015 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Bonafide Service. +""" + +from leap.bonafide._protocol import BonafideProtocol + +from twisted.application import service + + +class BonafideService(service.Service): + + # TODO inherit from HookableService (from common) + + def __init__(self): + self._bonafide = BonafideProtocol() + self.service_hooks = {} + + def register_hook(self, kind, trigger): + self.service_hooks[kind] = trigger + + def get_hooked_service(self, kind): + hooks = self.service_hooks + if kind in hooks: + return self.get_sibling_service(hooks[kind]) + + def get_sibling_service(self, kind): + return self.parent.getServiceNamed(kind) + + # Commands + + def do_authenticate(self, username, password): + + def notify_passphrase_entry(username, password): + this_hook = 'on_passphrase_entry' + hooked_service = self.get_hooked_service(this_hook) + if hooked_service: + hooked_service.notify_hook( + this_hook, username=username, password=password) + + def notify_bonafide_auth_hook(result): + this_hook = 'on_bonafide_auth' + token, uuid = result + hooked_service = self.get_hooked_service(this_hook) + if hooked_service: + hooked_service.notify_hook( + this_hook, + username=username, token=token, uuid=uuid, + password=password) + return result + + # XXX I still have doubts from where it's best to trigger this. + # We probably should wait for BOTH deferreds and + # handle local and remote authentication success together + # (and fail if either one fails). Going with fire-and-forget for + # now, but needs needs improvement. + + notify_passphrase_entry(username, password) + + d = self._bonafide.do_authenticate(username, password) + d.addCallback(notify_bonafide_auth_hook) + d.addCallback(lambda response: 'TOKEN, UUID: %s' % str(response)) + return d + + def do_signup(self, username, password): + d = self._bonafide.do_signup(username, password) + d.addCallback(lambda response: 'REGISTERED -> %s' % response) + return d + + def do_logout(self, username, password): + d = self._bonafide.do_logout(username, password) + d.addCallback(lambda response: 'LOGOUT -> ok') + return d diff --git a/bonafide/src/leap/bonafide/zmq_service.py b/bonafide/src/leap/bonafide/zmq_service.py deleted file mode 100644 index f2cf123..0000000 --- a/bonafide/src/leap/bonafide/zmq_service.py +++ /dev/null @@ -1,166 +0,0 @@ -# -*- coding: utf-8 -*- -# zmq_service.py -# Copyright (C) 2015 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Bonafide ZMQ Service. -""" - -from leap.bonafide import config -from leap.bonafide._protocol import BonafideProtocol, COMMANDS - -from txzmq import ZmqEndpoint, ZmqFactory, ZmqREPConnection - -from twisted.application import service -from twisted.internet import reactor -from twisted.python import log - - -class BonafideZMQService(service.Service): - - def __init__(self): - self._bonafide = BonafideProtocol() - self._conn = None - - self.service_hooks = {} - - def startService(self): - zf = ZmqFactory() - e = ZmqEndpoint("bind", config.ENDPOINT) - - self._conn = _BonafideZmqREPConnection(zf, e, self._bonafide, self) - reactor.callWhenRunning(self._conn.do_greet) - super(BonafideZMQService, self).startService() - - def stopService(self): - super(BonafideZMQService, self).stopService() - - def register_hook(self, kind, trigger): - self.service_hooks[kind] = trigger - - - -class _BonafideZmqREPConnection(ZmqREPConnection): - - def __init__(self, zf, e, bonafide, service): - # XXX passing a ref to the service, - # to be able to access sibling services - ZmqREPConnection.__init__(self, zf, e) - self._bonafide = bonafide - self._service = service - - def get_sibling_service(self, kind): - return self._service.parent.getServiceNamed(kind) - - def get_hooked_service(self, kind): - hooks = self._service.service_hooks - if kind in hooks: - return self.get_sibling_service(hooks[kind]) - - def do_greet(self): - print "Starting Bonafide service" - - def do_bye(self): - print "Bonafide service stopped. Have a nice day." - reactor.stop() - - def defer_reply(self, response, msgId): - reactor.callLater(0, self.reply, msgId, str(response)) - - def log_err(self, failure, msgId): - log.err(failure) - self.defer_reply("ERROR: %r" % failure, msgId) - - def gotMessage(self, msgId, *parts): - - cmd = parts[0] - - if cmd == "shutdown": - self.do_shutdown(msgId) - - if cmd not in COMMANDS + ("get_soledad",): - response = 'INVALID COMMAND' - self.defer_reply(response, msgId) - - elif cmd == 'signup': - self.do_signup(parts, msgId) - - elif cmd == 'authenticate': - self.do_authenticate(parts, msgId) - - elif cmd == 'logout': - self.do_logout(self, parts, msgId) - - elif cmd == 'stats': - self.do_stats(msgId) - - def do_shutdown(self, msgId): - self.defer_reply('ok, shutting down', msgId) - reactor.callLater(1, self.do_bye) - - def do_signup(self, parts, msgId): - username, password = parts[1], parts[2] - d = self._bonafide.do_signup(username, password) - d.addCallback(lambda response: self.defer_reply( - 'REGISTERED -> %s' % response), msgId) - d.addErrback(self.log_err, msgId) - - def do_authenticate(self, parts, msgId): - - username, password = parts[1], parts[2] - - def notify_passphrase_entry(username, password): - this_hook = 'on_passphrase_entry' - hooked_service = self.get_hooked_service(this_hook) - if hooked_service: - hooked_service.notify_hook( - this_hook, username=username, password=password) - - def notify_bonafide_auth_hook(result): - this_hook = 'on_bonafide_auth' - token, uuid = result - hooked_service = self.get_hooked_service(this_hook) - if hooked_service: - hooked_service.notify_hook( - this_hook, - username=username, token=token, uuid=uuid, - password=password) - return result - - # XXX I still have doubts from where it's best to trigger this. - # We probably should wait for BOTH deferreds and - # handle local and remote authentication success together - # (and fail if either one fails). Going with fire-and-forget for - # now, but needs needs improvement. - - notify_passphrase_entry(username, password) - - d = self._bonafide.do_authenticate(username, password) - d.addCallback(notify_bonafide_auth_hook) - d.addCallback(lambda response: self.defer_reply( - 'TOKEN, UUID: %s' % str(response), msgId)) - d.addErrback(self.log_err, msgId) - - def do_logout(self, parts, msgId): - username, password = parts[1], parts[2] - d = self._bonafide.do_logout(username, password) - d.addCallback(lambda response: self.defer_reply( - 'LOGOUT -> ok'), msgId) - d.addErrback(self.log_err, msgId) - - def do_stats(self, msgId): - response = self._bonafide.do_stats() - self.defer_reply(response, msgId) -- cgit v1.2.3 From 930c206b982bc0ebf91751a0d5e8316308344ba2 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 4 Dec 2015 00:19:58 -0400 Subject: minor style fix for auth result --- bonafide/src/leap/bonafide/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index b681a58..36399de 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -75,7 +75,8 @@ class BonafideService(service.Service): d = self._bonafide.do_authenticate(username, password) d.addCallback(notify_bonafide_auth_hook) - d.addCallback(lambda response: 'TOKEN, UUID: %s' % str(response)) + d.addCallback(lambda response: '[ SRP TOKEN: %s ] [ UUID: %s ]' % + (response[0], response[1])) return d def do_signup(self, username, password): -- cgit v1.2.3 From 02262afe5bcf4726966187a0e1efe4df5c6ff215 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Dec 2015 12:00:03 -0400 Subject: delete bonafide cli (use bitmask.core instead) --- bonafide/src/leap/bonafide/bonafide_cli.py | 110 -------------------------- bonafide/src/leap/bonafide/bonafide_cli2 | 120 ----------------------------- 2 files changed, 230 deletions(-) delete mode 100644 bonafide/src/leap/bonafide/bonafide_cli.py delete mode 100755 bonafide/src/leap/bonafide/bonafide_cli2 diff --git a/bonafide/src/leap/bonafide/bonafide_cli.py b/bonafide/src/leap/bonafide/bonafide_cli.py deleted file mode 100644 index 3f500e4..0000000 --- a/bonafide/src/leap/bonafide/bonafide_cli.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# bonafide_cli.py -# Copyright (C) 2015 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Command Line interface for bonafide cli. -""" - -# XXX Warning! Work in progress------------------------------------------------ -# This is just a demo, real cli will work against a persistent bonafide daemon. -# XXX ------------------------------------------------------------------------- - -import argparse -import os -import sys -from getpass import getpass - -from colorama import init as color_init -from colorama import Fore - -from twisted.cred.credentials import UsernamePassword -from twisted.internet import reactor - -from leap.bonafide import provider -from leap.bonafide import session - -COMMANDS = ('signup', 'authenticate') - - -def _cbShutDown(ignored): - reactor.stop() - - -def _authEb(failure): - print(Fore.RED + "[error] " + Fore.YELLOW + - failure.getErrorMessage() + Fore.RESET) - - -def _display_token(result, _session): - if result == session.OK: - print('[ok] token--> ' + Fore.GREEN + - _session.token + Fore.RESET) - print('[ok] uuid --> ' + Fore.GREEN + - _session.uuid + Fore.RESET) - -def _display_registered(result, _session, _provider): - ok, user = result - if ok == session.OK: - print('[ok] registered username--> ' + Fore.GREEN + - '%s@%s' % (user, _provider)) - - -def run_command(command, _provider, username, password, skip_logout): - api = provider.Api('https://api.%s:4430' % _provider) - credentials = UsernamePassword(username, password) - cdev_pem = os.path.expanduser( - '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) - _session = session.Session(credentials, api, cdev_pem) - - if command == 'authenticate': - d = _session.authenticate() - d.addCallback(_display_token, _session) - elif command == 'signup': - d = _session.signup(username, password) - d.addCallback(_display_registered, _session, _provider) - else: - print(Fore.YELLOW + "Command not implemented" + Fore.RESET) - sys.exit() - - d.addErrback(_authEb) - if not skip_logout: - d.addCallback(lambda _: _session.logout()) - d.addBoth(_cbShutDown) - reactor.run() - - -def main(): - color_init() - description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' - 'using the bonafide protocol.' + Fore.RESET) - parser = argparse.ArgumentParser(description=description) - parser.add_argument('command', type=str, choices=COMMANDS) - parser.add_argument( - '--skip-logout', action="store_true", - help=("Skip logout. Use this if you want to re-use the token.")) - parser.add_argument('--provider', dest='provider', required=True) - parser.add_argument('--username', dest='username', required=True) - - ns = parser.parse_args() - password = getpass( - Fore.BLUE + '%s@%s password:' % ( - ns.username, ns.provider) + Fore.RESET) - run_command(ns.command, ns.provider, ns.username, password, ns.skip_logout) - - -if __name__ == '__main__': - main() diff --git a/bonafide/src/leap/bonafide/bonafide_cli2 b/bonafide/src/leap/bonafide/bonafide_cli2 deleted file mode 100755 index bf05a99..0000000 --- a/bonafide/src/leap/bonafide/bonafide_cli2 +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# bonafide_cli2.py -# Copyright (C) 2015 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Bonafide command line interface: zmq client. -""" -import sys -import getpass -import argparse - -from colorama import init as color_init -from colorama import Fore -from twisted.internet import reactor -from txzmq import ZmqEndpoint, ZmqFactory, ZmqREQConnection -import zmq - -from leap.bonafide import config - -description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' - 'using the bonafide protocol. This client connects to ' - 'a running Bonafide service.' + Fore.RESET) - -parser = argparse.ArgumentParser(description=description) -parser.add_argument("--stats", dest="do_stats", action="store_true", - help="print service stats") -parser.add_argument("--signup", action="store_true", dest="do_signup", - help="signup new user") -parser.add_argument("--auth", dest="do_auth", action="store_true", - help="authenticate the passed user") -parser.add_argument("--logout", dest="do_logout", action="store_true", - help="logout this user") -parser.add_argument("--username", dest="username", - help="user to operate with") -parser.add_argument("--shutdown", dest="do_shutdown", action="store_true", - help="shutdown the bonafide service.") - -# XXX DEBUG -------------------------------------------------------- -parser.add_argument("--debug", dest="do_debug", action="store_true", - help="debug command, can be anything") -# ------------------------------------------------------------------ -ns = parser.parse_args() - - -def get_zmq_connection(): - zf = ZmqFactory() - e = ZmqEndpoint('connect', config.ENDPOINT) - return ZmqREQConnection(zf, e) - - -def error(msg): - print Fore.RED + "[!] %s" % msg + Fore.RESET - sys.exit(1) - -if len(sys.argv) < 2: - error("Too few arguments. Try %s --help" % sys.argv[0]) - - -if (ns.do_signup or ns.do_auth or ns.do_logout) and not ns.username: - error(Fore.RED + "Need to pass a username for signup/auth/logout" + - Fore.RESET) - -if ns.username and '@' not in ns.username: - error(Fore.RED + "Username must be in the form user@provider" + Fore.RESET) - - -def do_print(stuff): - print Fore.GREEN + stuff[0] + Fore.RESET - - -def send_command(): - - cb = do_print - if ns.do_shutdown: - data = ("shutdown",) - - elif ns.do_stats: - data = ("stats",) - - elif ns.do_signup: - passwd = getpass.getpass() - data = ("signup", ns.username, passwd) - - elif ns.do_auth: - passwd = getpass.getpass() - data = ("authenticate", ns.username, passwd) - - elif ns.do_logout: - passwd = getpass.getpass() - data = ("logout", ns.username, passwd) - - elif ns.do_debug: - data = ("get_soledad",) - - s = get_zmq_connection() - try: - d = s.sendMsg(*data) - except zmq.error.Again: - print Fore.RED + "[ERROR] Server is down" + Fore.RESET - d.addCallback(cb) - d.addCallback(lambda x: reactor.stop()) - - -if __name__ == "__main__": - color_init() - reactor.callWhenRunning(reactor.callLater, 0, send_command) - reactor.run() -- cgit v1.2.3 From 51b3c7c7677a4acf2565b480d64d473e4ba6bcf1 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Dec 2015 12:31:51 -0400 Subject: use bitmask.core to test bonafide --- bonafide/README.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bonafide/README.rst b/bonafide/README.rst index 2e10764..554bb37 100644 --- a/bonafide/README.rst +++ b/bonafide/README.rst @@ -18,8 +18,10 @@ the package in development mode by running:: from the parent folder. -To run the bonafide daemon:: +To run the bonafide service, you can use the bitmask.core daemon. From the root +of the bitmask_core repo:: - make bonafide_server + make bitmaskd -Then you can use `bonafide_cli2 -h` to see the available commands. +Then you can use `bitmask_cli` to see the available actions, under the user +command. -- cgit v1.2.3 From d9b4b041035c018d3b24b4866647df6092c8f55b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Dec 2015 12:33:13 -0400 Subject: accept basedir in service initialization --- bonafide/src/leap/bonafide/service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 36399de..2ff4eed 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -19,6 +19,8 @@ Bonafide Service. """ +import os + from leap.bonafide._protocol import BonafideProtocol from twisted.application import service @@ -28,8 +30,9 @@ class BonafideService(service.Service): # TODO inherit from HookableService (from common) - def __init__(self): + def __init__(self, basedir='~/.config/leap'): self._bonafide = BonafideProtocol() + self._basedir = os.path.expanduser(basedir) self.service_hooks = {} def register_hook(self, kind, trigger): -- cgit v1.2.3 From c4984f7e631f770f2bef17f037a23b9347fec85f Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Wed, 16 Dec 2015 12:27:46 -0400 Subject: add command to download smtp certificate --- bonafide/src/leap/bonafide/_protocol.py | 19 +++++++++++-------- bonafide/src/leap/bonafide/service.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index 617c93c..f1c3834 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -114,17 +114,20 @@ class BonafideProtocol(object): d.addCallback(lambda _: '%s logged out' % full_id) return d - def do_stats(self): - log.msg('Calculating Bonafide Service STATS') - mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss - return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( - len(self._sessions), mem / 1024) + def do_get_smtp_cert(self, full_id): + session = self._get_session(full_id) + d = session.get_smtp_cert() + return d def do_get_vpn_cert(self): pass - def do_get_smtp_cert(self): - pass - def do_update_user(self): pass + + def do_stats(self): + log.msg('Calculating Bonafide Service STATS') + mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( + len(self._sessions), mem / 1024) + diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 2ff4eed..468721b 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -24,6 +24,7 @@ import os from leap.bonafide._protocol import BonafideProtocol from twisted.application import service +from twisted.internet import defer class BonafideService(service.Service): @@ -35,6 +36,10 @@ class BonafideService(service.Service): self._basedir = os.path.expanduser(basedir) self.service_hooks = {} + # XXX this is a quick hack to get a ref + # to the latest authenticated user. + self._active_user = None + def register_hook(self, kind, trigger): self.service_hooks[kind] = trigger @@ -66,6 +71,7 @@ class BonafideService(service.Service): this_hook, username=username, token=token, uuid=uuid, password=password) + self._active_user = username return result # XXX I still have doubts from where it's best to trigger this. @@ -91,3 +97,14 @@ class BonafideService(service.Service): d = self._bonafide.do_logout(username, password) d.addCallback(lambda response: 'LOGOUT -> ok') return d + + def do_get_smtp_cert(self, username=None): + if not username: + username = self._active_user + if not username: + return defer.fail( + RuntimeError('No active user, cannot get SMTP cert.')) + + d = self._bonafide.do_get_smtp_cert(username) + d.addCallback(lambda response: (username, response)) + return d -- cgit v1.2.3 From 49656a828be9a0243c4a4a5a939f5d7de386023e Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 18 Dec 2015 02:00:58 -0400 Subject: provider service bootstrap and autodiscovery --- bonafide/src/leap/bonafide/_protocol.py | 27 +++- bonafide/src/leap/bonafide/config.py | 260 +++++++++++++++++++++++++++++--- bonafide/src/leap/bonafide/provider.py | 80 ++++++++-- bonafide/src/leap/bonafide/service.py | 5 + 4 files changed, 328 insertions(+), 44 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index f1c3834..de959c4 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -45,6 +45,7 @@ class BonafideProtocol(object): _sessions = defaultdict(None) def _get_api(self, provider_id): + # TODO should get deferred if provider_id in self._apis: return self._apis[provider_id] @@ -73,26 +74,45 @@ class BonafideProtocol(object): # Service public methods def do_signup(self, full_id, password): + log.msg('SIGNUP for %s' % full_id) + _, provider_id = config.get_username_and_provider(full_id) + + provider = config.Provider(provider_id) + d = provider.callWhenReady(self._do_signup, full_id, password) + return d + + def _do_signup(self, full_id, password): + # XXX check it's unauthenticated def return_user(result, _session): return_code, user = result if return_code == OK: return user - log.msg('SIGNUP for %s' % full_id) + username, _ = config.get_username_and_provider(full_id) + # XXX get deferred? session = self._get_session(full_id, password) - username, provider_id = config.get_username_and_provider(full_id) - d = session.signup(username, password) d.addCallback(return_user, session) return d def do_authenticate(self, full_id, password): + log.msg('SIGNUP for %s' % full_id) + _, provider_id = config.get_username_and_provider(full_id) + + provider = config.Provider(provider_id) + d = provider.callWhenReady(self._do_authenticate, full_id, password) + return d + + def _do_authenticate(self, full_id, password): + def return_token_and_uuid(result, _session): if result == OK: return str(_session.token), str(_session.uuid) log.msg('AUTH for %s' % full_id) + + # XXX get deferred? session = self._get_session(full_id, password) d = session.authenticate() d.addCallback(return_token_and_uuid, session) @@ -130,4 +150,3 @@ class BonafideProtocol(object): mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( len(self._sessions), mem / 1024) - diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index 9490f55..afc59fc 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -18,14 +18,22 @@ Configuration for a LEAP provider. """ import datetime +import json import os import sys +from twisted.internet import defer, reactor +from twisted.internet.ssl import ClientContextFactory +from twisted.python import log +from twisted.web.client import Agent, downloadPage + from leap.bonafide._http import httpRequest +from leap.bonafide.provider import Discovery from leap.common.check import leap_assert from leap.common.config import get_path_prefix as common_get_path_prefix -from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p +from leap.common.files import mkdir_p +# check_and_fix_urw_only, get_mtime APPNAME = "bonafide" @@ -36,17 +44,24 @@ def get_path_prefix(standalone=False): return common_get_path_prefix(standalone) -def get_provider_path(domain): +def get_provider_path(domain, config='provider.json'): """ - Returns relative path for provider config. + Returns relative path for provider configs. :param domain: the domain to which this providerconfig belongs to. :type domain: str :returns: the path :rtype: str """ - leap_assert(domain is not None, "get_provider_path: We need a domain") - return os.path.join("leap", "providers", domain, "provider.json") + # TODO sanitize domain + leap_assert(domain is not None, 'get_provider_path: We need a domain') + return os.path.join('providers', domain, config) + + +def get_ca_cert_path(domain): + # TODO sanitize domain + leap_assert(domain is not None, 'get_provider_path: We need a domain') + return os.path.join('providers', domain, 'keys', 'ca', 'cacert.pem') def get_modification_ts(path): @@ -99,28 +114,88 @@ def make_address(user, provider): :param provider: the provider domain :type provider: basestring """ - return "%s@%s" % (user, provider) + return '%s@%s' % (user, provider) def get_username_and_provider(full_id): return full_id.split('@') -class ProviderConfig(object): - # TODO add file config for enabled services +class WebClientContextFactory(ClientContextFactory): + def getContext(self, hostname, port): + return ClientContextFactory.getContext(self) + + +class Provider(object): + # TODO split Provider, ProviderConfig + # TODO add validation - def __init__(self, domain): - self._api_base = None + SERVICES_MAP = { + 'openvpn': ['eip'], + 'mx': ['soledad', 'smtp']} + + def __init__(self, domain, autoconf=True, basedir='~/.config/leap', + check_certificate=True): self._domain = domain + self._basedir = os.path.expanduser(basedir) + self._disco = Discovery('https://%s' % domain) + self._provider_config = {} + + is_configured = self.is_configured() + if not is_configured: + check_certificate = False + + if check_certificate: + self.contextFactory = None + else: + # XXX we should do this only for the FIRST provider download. + # For the rest, we should pass the ca cert to the agent. + # That means that RIGHT AFTER DOWNLOADING provider_info, + # we should instantiate a new Agent... + self.contextFactory = WebClientContextFactory() + self._agent = Agent(reactor, self.contextFactory) + + self._load_provider_config() + # TODO if loaded, setup _get_api_uri on the DISCOVERY + + self._init_deferred = None + + if not is_configured and autoconf: + print 'provider %s not configured: downloading files...' % domain + self.bootstrap() + else: + print 'already initialized' + self._init_deferred = defer.succeed('already_initialized') + + def callWhenReady(self, cb, *args, **kw): + print 'calling when ready', cb + d = self._init_deferred + d.addCallback(lambda _: cb(*args, **kw)) + d.addErrback(log.err) + return d def is_configured(self): provider_json = self._get_provider_json_path() # XXX check if all the services are there - if is_file(provider_json): - return True - return False + if not is_file(provider_json): + return False + if not is_file(self._get_ca_cert_path()): + return False + return True + + def bootstrap(self): + print "Bootstrapping provider %s" % self._domain + d = self.maybe_download_provider_info() + d.addCallback(self.maybe_download_ca_cert) + d.addCallback(self.validate_ca_cert) + d.addCallback(self.maybe_download_services_config) + d.addCallback(self.load_services_config) + self._init_deferred = d + + def has_valid_certificate(self): + pass - def download_provider_info(self): + def maybe_download_provider_info(self, replace=False): """ Download the provider.json info from the main domain. This SHOULD only be used once with the DOMAIN url. @@ -128,8 +203,19 @@ class ProviderConfig(object): # TODO handle pre-seeded providers? # or let client handle that? We could move them to bonafide. provider_json = self._get_provider_json_path() - if is_file(provider_json): - raise RuntimeError('File already exists') + if is_file(provider_json) and not replace: + return defer.succeed('provider_info_already_exists') + + folders, f = os.path.split(provider_json) + mkdir_p(folders) + + uri = self._disco.get_provider_info_uri() + met = self._disco.get_provider_info_method() + + d = downloadPage(uri, provider_json, method=met) + d.addCallback(lambda _: self._load_provider_config()) + d.addErrback(log.err) + return d def update_provider_info(self): """ @@ -137,16 +223,142 @@ class ProviderConfig(object): """ pass - def _http_request(self, *args, **kw): - # XXX pass if-modified-since header - return httpRequest(*args, **kw) + def maybe_download_ca_cert(self, ignored): + """ + :rtype: deferred + """ + path = self._get_ca_cert_path() + if is_file(path): + return defer.succeed('ca_cert_path_already_exists') + + uri = self._get_ca_cert_uri() + mkdir_p(os.path.split(path)[0]) + d = downloadPage(uri, path) + d.addErrback(log.err) + return d + + def validate_ca_cert(self, ignored): + # XXX Need to verify fingerprint against the one in provider.json + expected = self._get_expected_ca_cert_fingerprint() + print "EXPECTED FINGERPRINT:", expected + + def _get_expected_ca_cert_fingerprint(self): + try: + fgp = self._provider_config.ca_cert_fingerprint + except AttributeError: + fgp = None + return fgp + + + def maybe_download_services_config(self, ignored): + pass + + def load_services_config(self, ignored): + print 'loading services config...' + configs_path = self._get_configs_path() + + uri = self._disco.get_configs_uri() + met = self._disco.get_configs_method() + + # TODO --- currently, provider on mail.bitmask.net raises 401 + # UNAUTHENTICATED if we try to # get the services on first boostrap. + # See: # https://leap.se/code/issues/7906 + + # As a Workaround, these urls work though: + # curl -k https://api.mail.bitmask.net:4430/1/config/smtp-service.json + # curl -k https://api.mail.bitmask.net:4430/1/config/soledad-service.json + + print "GETTING SERVICES FROM...", uri + + d = downloadPage(uri, configs_path, method=met) + d.addCallback(lambda _: self._load_provider_config()) + d.addCallback(lambda _: self._get_config_for_all_services()) + d.addErrback(log.err) + return d + + def offers_service(self, service): + if service not in self.SERVICES_MAP.keys(): + raise RuntimeError('Unknown service: %s' % service) + return service in self._provider_config.services + + # TODO is_service_enabled ---> this belongs to core? def _get_provider_json_path(self): domain = self._domain.encode(sys.getfilesystemencoding()) - provider_json = os.path.join(get_path_prefix(), get_provider_path(domain)) - return provider_json + provider_json_path = os.path.join( + self._basedir, get_provider_path(domain, config='provider.json')) + return provider_json_path + + def _get_configs_path(self): + domain = self._domain.encode(sys.getfilesystemencoding()) + configs_path = os.path.join( + self._basedir, get_provider_path(domain, config='configs.json')) + return configs_path + + def _get_service_config_path(self, service): + domain = self._domain.encode(sys.getfilesystemencoding()) + configs_path = os.path.join( + self._basedir, get_provider_path( + domain, config='%s-service.json' % service)) + return configs_path + + def _get_ca_cert_path(self): + domain = self._domain.encode(sys.getfilesystemencoding()) + cert_path = os.path.join(self._basedir, get_ca_cert_path(domain)) + return cert_path + + def _get_ca_cert_uri(self): + try: + uri = self._provider_config.ca_cert_uri + uri = str(uri) + except Exception: + uri = None + return uri + + def _load_provider_config(self): + path = self._get_provider_json_path() + if not is_file(path): + return + with open(path, 'r') as config: + self._provider_config = Record(**json.load(config)) + + def _get_config_for_all_services(self): + configs_path = self._get_configs_path() + with open(configs_path) as jsonf: + services_dict = Record(**json.load(jsonf)).services + pending = [] + base = self._disco.get_base_uri() + for service in self._provider_config.services: + for subservice in self.SERVICES_MAP[service]: + uri = base + str(services_dict[subservice]) + path = self._get_service_config_path(subservice) + d = self._fetch_config_for_service(uri, path) + pending.append(d) + return defer.gatherResults(pending) + + def _fetch_config_for_service(self, uri, path): + log.msg('Downloading config for %s...' % uri) + d = downloadPage(uri, path, method='GET') + return d + + def _http_request(self, *args, **kw): + # XXX pass if-modified-since header + return httpRequest(self._agent, *args, **kw) + + def _get_api_uri(self): + pass + + +class Record(object): + def __init__(self, **kw): + self.__dict__.update(kw) + if __name__ == '__main__': - config = ProviderConfig('cdev.bitmask.net') - config.is_configured() - config.download_provider_info() + + def print_done(): + print '>>> bootstrapping done!!!' + + provider = Provider('cdev.bitmask.net') + provider.callWhenReady(print_done) + reactor.run() diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py index 1d0b5b7..7e78196 100644 --- a/bonafide/src/leap/bonafide/provider.py +++ b/bonafide/src/leap/bonafide/provider.py @@ -38,11 +38,13 @@ class _MetaActionDispatcher(type): where `uri_template` is a string that will be formatted with an arbitrary number of keyword arguments. - Any class that uses this one as its metaclass needs to implement two private - methods:: + Any class that uses this one as its metaclass needs to implement two + private methods:: _get_uri(self, action_name, **extra_params) _get_method(self, action_name) + + Beware that currently they cannot be inherited from bases. """ def __new__(meta, name, bases, dct): @@ -74,17 +76,34 @@ class _MetaActionDispatcher(type): meta, name, bases, newdct) -class Api(object): +class BaseProvider(object): + + def __init__(self, netloc, version=1): + parsed = urlparse(netloc) + if parsed.scheme != 'https': + raise ValueError( + 'ProviderApi needs to be passed a url with https scheme') + self.netloc = parsed.netloc + self.version = version + + def get_hostname(self): + return urlparse(self._get_base_url()).hostname + + def _get_base_url(self): + return "https://{0}/{1}".format(self.netloc, self.version) + + +class Api(BaseProvider): """ An object that has all the information that a client needs to communicate with the remote methods exposed by the web API of a LEAP provider. The actions are described in https://leap.se/bonafide - By using the _MetaActionDispatcher as a metaclass, the _actions dict will be - translated dynamically into a set of instance methods that will allow + By using the _MetaActionDispatcher as a metaclass, the _actions dict will + be translated dynamically into a set of instance methods that will allow getting the uri and method for each action. - + The keyword arguments specified in the format string will automatically raise a KeyError if the needed keyword arguments are not passed to the dynamically created methods. @@ -106,19 +125,48 @@ class Api(object): 'smtp_cert': ('smtp_cert', 'POST'), } - def __init__(self, netloc, version=1): - parsed = urlparse(netloc) - if parsed.scheme != 'https': - raise ValueError( - 'ProviderApi needs to be passed a url with https scheme') - self.netloc = parsed.netloc - self.version = version + # Methods expected by the dispatcher metaclass - def get_hostname(self): - return urlparse(self._get_base_url()).hostname + def _get_uri(self, action_name, **extra_params): + resource, _ = self._actions.get(action_name) + uri = '{0}/{1}'.format( + bytes(self._get_base_url()), + bytes(resource)).format(**extra_params) + return uri + + def _get_method(self, action_name): + _, method = self._actions.get(action_name) + return method + + +class Discovery(BaseProvider): + """ + Discover basic information about a provider, including the provided + services. + """ + + __metaclass__ = _MetaActionDispatcher + _actions = { + 'provider_info': ('provider.json', 'GET'), + 'configs': ('1/configs.json', 'GET'), + } + + api_uri = None + api_port = None def _get_base_url(self): - return "https://{0}/{1}".format(self.netloc, self.version) + if self.api_uri: + base = self.api_uri + else: + base = self.netloc + + uri = "https://{0}".format(base) + if self.api_port: + uri = uri + ':%s' % self.api_port + return uri + + def get_base_uri(self): + return self._get_base_url() # Methods expected by the dispatcher metaclass diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 468721b..bdbf729 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -25,6 +25,7 @@ from leap.bonafide._protocol import BonafideProtocol from twisted.application import service from twisted.internet import defer +from twisted.python import log class BonafideService(service.Service): @@ -63,6 +64,10 @@ class BonafideService(service.Service): this_hook, username=username, password=password) def notify_bonafide_auth_hook(result): + if not result: + log.msg("Authentication hook did not return anything") + return + this_hook = 'on_bonafide_auth' token, uuid = result hooked_service = self.get_hooked_service(this_hook) -- cgit v1.2.3 From 4a195782cd464390bf79bd52ae7903629bd879fe Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Wed, 17 Feb 2016 11:11:09 -0400 Subject: two-step bootstrap if needs authentication --- bonafide/src/leap/bonafide/_protocol.py | 12 +- bonafide/src/leap/bonafide/config.py | 218 +++++++++++++++++++++++++------- bonafide/src/leap/bonafide/session.py | 9 ++ 3 files changed, 191 insertions(+), 48 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index de959c4..e66be19 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -101,13 +101,23 @@ class BonafideProtocol(object): _, provider_id = config.get_username_and_provider(full_id) provider = config.Provider(provider_id) - d = provider.callWhenReady(self._do_authenticate, full_id, password) + + def maybe_finish_provider_bootstrap(result, provider): + session = self._get_session(full_id, password) + d = provider.download_services_config_with_auth(session) + d.addCallback(lambda _: result) + return d + + d = provider.callWhenMainConfigReady( + self._do_authenticate, full_id, password) + d.addCallback(maybe_finish_provider_bootstrap, provider) return d def _do_authenticate(self, full_id, password): def return_token_and_uuid(result, _session): if result == OK: + # TODO -- turn this into JSON response return str(_session.token), str(_session.uuid) log.msg('AUTH for %s' % full_id) diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index afc59fc..0f410f3 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -22,6 +22,9 @@ import json import os import sys +from collections import defaultdict +from urlparse import urlparse + from twisted.internet import defer, reactor from twisted.internet.ssl import ClientContextFactory from twisted.python import log @@ -127,13 +130,16 @@ class WebClientContextFactory(ClientContextFactory): class Provider(object): - # TODO split Provider, ProviderConfig # TODO add validation SERVICES_MAP = { 'openvpn': ['eip'], 'mx': ['soledad', 'smtp']} + first_bootstrap = defaultdict(None) + ongoing_bootstrap = defaultdict(None) + stuck_bootstrap = defaultdict(None) + def __init__(self, domain, autoconf=True, basedir='~/.config/leap', check_certificate=True): self._domain = domain @@ -155,24 +161,18 @@ class Provider(object): self.contextFactory = WebClientContextFactory() self._agent = Agent(reactor, self.contextFactory) - self._load_provider_config() - # TODO if loaded, setup _get_api_uri on the DISCOVERY - - self._init_deferred = None + self._load_provider_json() if not is_configured and autoconf: - print 'provider %s not configured: downloading files...' % domain + log.msg('provider %s not configured: downloading files...' % + domain) self.bootstrap() else: - print 'already initialized' - self._init_deferred = defer.succeed('already_initialized') - - def callWhenReady(self, cb, *args, **kw): - print 'calling when ready', cb - d = self._init_deferred - d.addCallback(lambda _: cb(*args, **kw)) - d.addErrback(log.err) - return d + log.msg('Provider already initialized') + self.first_bootstrap[self._domain] = defer.succeed( + 'already_initialized') + self.ongoing_bootstrap[self._domain] = defer.succeed( + 'already_initialized') def is_configured(self): provider_json = self._get_provider_json_path() @@ -181,16 +181,44 @@ class Provider(object): return False if not is_file(self._get_ca_cert_path()): return False + if not self.has_config_for_all_services(): + return False return True def bootstrap(self): - print "Bootstrapping provider %s" % self._domain + domain = self._domain + log.msg("Bootstrapping provider %s" % domain) + ongoing = self.ongoing_bootstrap.get(domain) + if ongoing: + log.msg('already bootstrapping this provider...') + return + + self.first_bootstrap[self._domain] = defer.Deferred() + + def first_bootstrap_done(ignored): + try: + self.first_bootstrap[domain].callback('got config') + except defer.AlreadyCalledError: + pass + d = self.maybe_download_provider_info() d.addCallback(self.maybe_download_ca_cert) d.addCallback(self.validate_ca_cert) + d.addCallback(first_bootstrap_done) d.addCallback(self.maybe_download_services_config) - d.addCallback(self.load_services_config) - self._init_deferred = d + self.ongoing_bootstrap[domain] = d + + def callWhenMainConfigReady(self, cb, *args, **kw): + d = self.first_bootstrap[self._domain] + d.addCallback(lambda _: cb(*args, **kw)) + d.addErrback(log.err) + return d + + def callWhenReady(self, cb, *args, **kw): + d = self.ongoing_bootstrap[self._domain] + d.addCallback(lambda _: cb(*args, **kw)) + d.addErrback(log.err) + return d def has_valid_certificate(self): pass @@ -213,7 +241,7 @@ class Provider(object): met = self._disco.get_provider_info_method() d = downloadPage(uri, provider_json, method=met) - d.addCallback(lambda _: self._load_provider_config()) + d.addCallback(lambda _: self._load_provider_json()) d.addErrback(log.err) return d @@ -238,7 +266,7 @@ class Provider(object): return d def validate_ca_cert(self, ignored): - # XXX Need to verify fingerprint against the one in provider.json + # TODO Need to verify fingerprint against the one in provider.json expected = self._get_expected_ca_cert_fingerprint() print "EXPECTED FINGERPRINT:", expected @@ -249,39 +277,116 @@ class Provider(object): fgp = None return fgp + # Services config files - def maybe_download_services_config(self, ignored): - pass - - def load_services_config(self, ignored): - print 'loading services config...' - configs_path = self._get_configs_path() + def has_fetched_services_config(self): + return os.path.isfile(self._get_configs_path()) - uri = self._disco.get_configs_uri() - met = self._disco.get_configs_method() + def maybe_download_services_config(self, ignored): - # TODO --- currently, provider on mail.bitmask.net raises 401 - # UNAUTHENTICATED if we try to # get the services on first boostrap. + # TODO --- currently, some providers (mail.bitmask.net) raise 401 + # UNAUTHENTICATED if we try to get the services # See: # https://leap.se/code/issues/7906 - # As a Workaround, these urls work though: - # curl -k https://api.mail.bitmask.net:4430/1/config/smtp-service.json - # curl -k https://api.mail.bitmask.net:4430/1/config/soledad-service.json + def further_bootstrap_needs_auth(ignored): + log.err('cannot download services config yet, need auth') + pending_deferred = defer.Deferred() + self.stuck_bootstrap[self._domain] = pending_deferred + return pending_deferred - print "GETTING SERVICES FROM...", uri + uri, met, path = self._get_configs_download_params() - d = downloadPage(uri, configs_path, method=met) - d.addCallback(lambda _: self._load_provider_config()) - d.addCallback(lambda _: self._get_config_for_all_services()) - d.addErrback(log.err) + d = downloadPage(uri, path, method=met) + d.addCallback(lambda _: self._load_provider_json()) + d.addCallback( + lambda _: self._get_config_for_all_services(session=None)) + d.addErrback(further_bootstrap_needs_auth) return d + def download_services_config_with_auth(self, session): + + def verify_provider_configs(ignored): + self._load_provider_configs() + return True + + def workaround_for_config_fetch(failure): + # FIXME --- configs.json raises 500, see #7914. + # This is a workaround until that's fixed. + log.err(failure) + log.msg("COULD NOT VERIFY CONFIGS.JSON, WORKAROUND: DIRECT DOWNLOAD") + + if 'mx' in self._provider_config.services: + soledad_uri = '/1/config/soledad-service.json' + smtp_uri = '/1/config/smtp-service.json' + base = self._disco.api_uri + + fetch = self._fetch_provider_configs_unauthenticated + get_path = self._get_service_config_path + + d1 = fetch('https://' + str(base + soledad_uri), get_path('soledad')) + d2 = fetch('https://' + str(base + smtp_uri), get_path('smtp')) + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: finish_stuck_after_workaround()) + return d + + def finish_stuck_after_workaround(): + stuck = self.stuck_bootstrap.get(self._domain, None) + if stuck: + stuck.callback('continue!') + + def complete_bootstrapping(ignored): + stuck = self.stuck_bootstrap.get(self._domain, None) + if stuck: + d = self._get_config_for_all_services(session) + d.addCallback(lambda _: stuck.callback('continue!')) + d.addErrback(log.err) + return d + + if not self.has_fetched_services_config(): + self._load_provider_json() + uri, met, path = self._get_configs_download_params() + d = session.fetch_provider_configs(uri, path) + d.addCallback(verify_provider_configs) + d.addCallback(complete_bootstrapping) + d.addErrback(workaround_for_config_fetch) + return d + else: + d = defer.succeed('already downloaded') + d.addCallback(complete_bootstrapping) + return d + + def _get_configs_download_params(self): + uri = self._disco.get_configs_uri() + met = self._disco.get_configs_method() + path = self._get_configs_path() + return uri, met, path + def offers_service(self, service): if service not in self.SERVICES_MAP.keys(): raise RuntimeError('Unknown service: %s' % service) return service in self._provider_config.services - # TODO is_service_enabled ---> this belongs to core? + def is_service_enabled(self, service): + # TODO implement on some config file + return True + + def has_config_for_service(self, service): + has_file = os.path.isfile + path = self._get_service_config_path + smap = self.SERVICES_MAP + + result = all([has_file(path(subservice)) for + subservice in smap[service]]) + return result + + def has_config_for_all_services(self): + if not self._provider_config: + return False + all_services = self._provider_config.services + has_all = all( + [self._has_config_for_service(service) for service in + all_services]) + return has_all def _get_provider_json_path(self): domain = self._domain.encode(sys.getfilesystemencoding()) @@ -315,28 +420,47 @@ class Provider(object): uri = None return uri - def _load_provider_config(self): + def _load_provider_json(self): path = self._get_provider_json_path() if not is_file(path): + log.msg("Cannot LOAD provider config path %s" % path) return + with open(path, 'r') as config: self._provider_config = Record(**json.load(config)) - def _get_config_for_all_services(self): + api_uri = self._provider_config.api_uri + if api_uri: + parsed = urlparse(api_uri) + self._disco.api_uri = parsed.netloc + + def _get_config_for_all_services(self, session): + services_dict = self._load_provider_configs() configs_path = self._get_configs_path() with open(configs_path) as jsonf: services_dict = Record(**json.load(jsonf)).services pending = [] base = self._disco.get_base_uri() for service in self._provider_config.services: - for subservice in self.SERVICES_MAP[service]: - uri = base + str(services_dict[subservice]) - path = self._get_service_config_path(subservice) - d = self._fetch_config_for_service(uri, path) - pending.append(d) + if service in self.SERVICES_MAP.keys(): + for subservice in self.SERVICES_MAP[service]: + uri = base + str(services_dict[subservice]) + path = self._get_service_config_path(subservice) + if session: + d = session.fetch_provider_configs(uri, path) + else: + d = self._fetch_provider_configs_unauthenticated( + uri, path) + pending.append(d) return defer.gatherResults(pending) - def _fetch_config_for_service(self, uri, path): + def _load_provider_configs(self): + configs_path = self._get_configs_path() + with open(configs_path) as jsonf: + services_dict = Record(**json.load(jsonf)).services + return services_dict + + def _fetch_provider_configs_unauthenticated(self, uri, path): log.msg('Downloading config for %s...' % uri) d = downloadPage(uri, path, method='GET') return d diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index 8343a91..7c40d28 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -156,6 +156,15 @@ class Session(object): def update_user_record(self): pass + # Authentication-protected configuration + + @defer.inlineCallbacks + def fetch_provider_configs(self, uri, path): + config = yield self._request(self._agent, uri) + with open(path, 'w') as cf: + cf.write(config) + defer.returnValue('ok') + if __name__ == "__main__": import os -- cgit v1.2.3 From 3d6b29a7c074ccd254a50c597146842629bc238d Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Thu, 25 Feb 2016 20:02:03 -0400 Subject: remove zmq tacfile, use core --- bonafide/src/leap/bonafide/zmq.tac | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 bonafide/src/leap/bonafide/zmq.tac diff --git a/bonafide/src/leap/bonafide/zmq.tac b/bonafide/src/leap/bonafide/zmq.tac deleted file mode 100644 index f429ff5..0000000 --- a/bonafide/src/leap/bonafide/zmq.tac +++ /dev/null @@ -1,18 +0,0 @@ -# Run as: twistd -n -y zmq.tac -from twisted.application import service -from leap.bonafide.zmq_service import BonafideZMQService -from leap.bonafide.soledad_service import SoledadService - -top_service = service.MultiService() -bonafide_zmq_service = BonafideZMQService() -bonafide_zmq_service.setServiceParent(top_service) - -# XXX DEBUG ------------------------------------- -# This SHOULD BE moved to BITMASK-CORE. -soledad_service = SoledadService() -soledad_service.setName("soledad") -soledad_service.setServiceParent(top_service) -#------------------------------------------------ - -application = service.Application("bonafide") -top_service.setServiceParent(application) -- cgit v1.2.3 From 30ac25cc88eba606c44984ef24395da652ab40ca Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Thu, 25 Feb 2016 20:02:31 -0400 Subject: remove ws service --- bonafide/src/leap/bonafide/ws_service.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 bonafide/src/leap/bonafide/ws_service.py diff --git a/bonafide/src/leap/bonafide/ws_service.py b/bonafide/src/leap/bonafide/ws_service.py deleted file mode 100644 index feaa4b0..0000000 --- a/bonafide/src/leap/bonafide/ws_service.py +++ /dev/null @@ -1 +0,0 @@ -# TODO expose bonafide protocol through websockets -- cgit v1.2.3 From 9cf90bdcb4469cbfea3265f2ade470e22b8d912f Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Thu, 25 Feb 2016 20:02:58 -0400 Subject: get active user --- bonafide/src/leap/bonafide/service.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index bdbf729..25ae9dc 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -52,6 +52,10 @@ class BonafideService(service.Service): def get_sibling_service(self, kind): return self.parent.getServiceNamed(kind) + def startService(self): + log.msg('Starting Bonafide Service') + super(BonafideService, self).startService() + # Commands def do_authenticate(self, username, password): @@ -99,7 +103,15 @@ class BonafideService(service.Service): return d def do_logout(self, username, password): + if not username: + username = self._active_user + + def reset_active(passthrough): + self._active_user = None + return passthrough + d = self._bonafide.do_logout(username, password) + d.addCallback(reset_active) d.addCallback(lambda response: 'LOGOUT -> ok') return d @@ -113,3 +125,7 @@ class BonafideService(service.Service): d = self._bonafide.do_get_smtp_cert(username) d.addCallback(lambda response: (username, response)) return d + + def do_get_active_user(self): + user = self._active_user + return user -- cgit v1.2.3 From d87ab57d3876d5246af5790aaa30dadeca06a2ed Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Tue, 1 Mar 2016 12:23:06 -0400 Subject: return deferred for user --- bonafide/src/leap/bonafide/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 25ae9dc..2548928 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -128,4 +128,4 @@ class BonafideService(service.Service): def do_get_active_user(self): user = self._active_user - return user + return defer.succeed(user) -- cgit v1.2.3 From bee54f6d751ea2cbad390fef7bb08f7ac31e3c76 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Tue, 1 Mar 2016 12:24:01 -0400 Subject: fix provider config check --- bonafide/src/leap/bonafide/config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index 0f410f3..f866368 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -145,7 +145,7 @@ class Provider(object): self._domain = domain self._basedir = os.path.expanduser(basedir) self._disco = Discovery('https://%s' % domain) - self._provider_config = {} + self._provider_config = None is_configured = self.is_configured() if not is_configured: @@ -380,11 +380,12 @@ class Provider(object): return result def has_config_for_all_services(self): + self._load_provider_json() if not self._provider_config: return False all_services = self._provider_config.services has_all = all( - [self._has_config_for_service(service) for service in + [self.has_config_for_service(service) for service in all_services]) return has_all -- cgit v1.2.3 From 002b673ca64024c87fb168cdd3d3635c22244158 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Wed, 2 Mar 2016 00:41:39 -0400 Subject: allow to notify multiple hooks --- bonafide/src/leap/bonafide/service.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 2548928..166127b 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -18,8 +18,8 @@ """ Bonafide Service. """ - import os +from collections import defaultdict from leap.bonafide._protocol import BonafideProtocol @@ -35,19 +35,21 @@ class BonafideService(service.Service): def __init__(self, basedir='~/.config/leap'): self._bonafide = BonafideProtocol() self._basedir = os.path.expanduser(basedir) - self.service_hooks = {} + self.service_hooks = defaultdict(list) # XXX this is a quick hack to get a ref # to the latest authenticated user. self._active_user = None def register_hook(self, kind, trigger): - self.service_hooks[kind] = trigger + log.msg("Registering hook %s->%s" % (kind, trigger)) + self.service_hooks[kind].append(trigger) - def get_hooked_service(self, kind): + def get_hooked_services(self, kind): hooks = self.service_hooks if kind in hooks: - return self.get_sibling_service(hooks[kind]) + names = hooks[kind] + return [self.get_sibling_service(name) for name in names] def get_sibling_service(self, kind): return self.parent.getServiceNamed(kind) @@ -62,9 +64,9 @@ class BonafideService(service.Service): def notify_passphrase_entry(username, password): this_hook = 'on_passphrase_entry' - hooked_service = self.get_hooked_service(this_hook) - if hooked_service: - hooked_service.notify_hook( + hooked_services = self.get_hooked_services(this_hook) + for service in hooked_services: + service.notify_hook( this_hook, username=username, password=password) def notify_bonafide_auth_hook(result): @@ -74,9 +76,9 @@ class BonafideService(service.Service): this_hook = 'on_bonafide_auth' token, uuid = result - hooked_service = self.get_hooked_service(this_hook) - if hooked_service: - hooked_service.notify_hook( + hooked_services = self.get_hooked_services(this_hook) + for service in hooked_services: + service.notify_hook( this_hook, username=username, token=token, uuid=uuid, password=password) @@ -93,8 +95,8 @@ class BonafideService(service.Service): d = self._bonafide.do_authenticate(username, password) d.addCallback(notify_bonafide_auth_hook) - d.addCallback(lambda response: '[ SRP TOKEN: %s ] [ UUID: %s ]' % - (response[0], response[1])) + d.addCallback(lambda response: { + 'srp_token': response[0], 'uuid': response[1]}) return d def do_signup(self, username, password): -- cgit v1.2.3 From 1fc1dac0ee65bcc42271804be7fd5e2679fd3eb1 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 00:45:27 -0400 Subject: use leap.common.service_hooks --- bonafide/src/leap/bonafide/service.py | 45 ++++++++++------------------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 166127b..87c2ef5 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -21,6 +21,7 @@ Bonafide Service. import os from collections import defaultdict +from leap.common.service_hooks import HookableService from leap.bonafide._protocol import BonafideProtocol from twisted.application import service @@ -28,11 +29,10 @@ from twisted.internet import defer from twisted.python import log -class BonafideService(service.Service): - - # TODO inherit from HookableService (from common) +class BonafideService(service.Service, HookableService): def __init__(self, basedir='~/.config/leap'): + # TODO fix hardcoded basedir self._bonafide = BonafideProtocol() self._basedir = os.path.expanduser(basedir) self.service_hooks = defaultdict(list) @@ -41,19 +41,6 @@ class BonafideService(service.Service): # to the latest authenticated user. self._active_user = None - def register_hook(self, kind, trigger): - log.msg("Registering hook %s->%s" % (kind, trigger)) - self.service_hooks[kind].append(trigger) - - def get_hooked_services(self, kind): - hooks = self.service_hooks - if kind in hooks: - names = hooks[kind] - return [self.get_sibling_service(name) for name in names] - - def get_sibling_service(self, kind): - return self.parent.getServiceNamed(kind) - def startService(self): log.msg('Starting Bonafide Service') super(BonafideService, self).startService() @@ -63,25 +50,19 @@ class BonafideService(service.Service): def do_authenticate(self, username, password): def notify_passphrase_entry(username, password): - this_hook = 'on_passphrase_entry' - hooked_services = self.get_hooked_services(this_hook) - for service in hooked_services: - service.notify_hook( - this_hook, username=username, password=password) + data = dict(username=username, password=password) + self.trigger_hook('on_passphrase_entry', **data) - def notify_bonafide_auth_hook(result): + def notify_bonafide_auth(result): if not result: log.msg("Authentication hook did not return anything") return - this_hook = 'on_bonafide_auth' token, uuid = result - hooked_services = self.get_hooked_services(this_hook) - for service in hooked_services: - service.notify_hook( - this_hook, - username=username, token=token, uuid=uuid, - password=password) + data = dict(username=username, token=token, uuid=uuid, + password=password) + self.trigger_hook('on_bonafide_auth', **data) + self._active_user = username return result @@ -94,14 +75,14 @@ class BonafideService(service.Service): notify_passphrase_entry(username, password) d = self._bonafide.do_authenticate(username, password) - d.addCallback(notify_bonafide_auth_hook) + d.addCallback(notify_bonafide_auth) d.addCallback(lambda response: { 'srp_token': response[0], 'uuid': response[1]}) return d def do_signup(self, username, password): d = self._bonafide.do_signup(username, password) - d.addCallback(lambda response: 'REGISTERED -> %s' % response) + d.addCallback(lambda response: {'signup': 'ok', 'user': response}) return d def do_logout(self, username, password): @@ -114,7 +95,7 @@ class BonafideService(service.Service): d = self._bonafide.do_logout(username, password) d.addCallback(reset_active) - d.addCallback(lambda response: 'LOGOUT -> ok') + d.addCallback(lambda response: {'logout': 'ok'}) return d def do_get_smtp_cert(self, username=None): -- cgit v1.2.3 From cb863861ecb9c5472f134f1026dba250541f4ff4 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 00:49:17 -0400 Subject: move decorator to session --- bonafide/src/leap/bonafide/_decorators.py | 33 ------------------------------- bonafide/src/leap/bonafide/session.py | 22 +++++++++++++++++---- 2 files changed, 18 insertions(+), 37 deletions(-) delete mode 100644 bonafide/src/leap/bonafide/_decorators.py diff --git a/bonafide/src/leap/bonafide/_decorators.py b/bonafide/src/leap/bonafide/_decorators.py deleted file mode 100644 index 6b43715..0000000 --- a/bonafide/src/leap/bonafide/_decorators.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# _decorators.py -# Copyright (C) 2015 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Decorators used in bonafide. -""" - - -def auth_required(func): - """ - Decorate a method so that it will not be called if the instance - attribute `is_authenticated` does not evaluate to True. - """ - def decorated(*args, **kwargs): - instance = args[0] - allowed = getattr(instance, 'is_authenticated') - if not allowed: - raise RuntimeError('This method requires authentication') - return func(*args, **kwargs) - return decorated diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index 7c40d28..547f0dd 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -22,12 +22,25 @@ from twisted.python import log from leap.bonafide import _srp from leap.bonafide import provider -from leap.bonafide._decorators import auth_required from leap.bonafide._http import httpRequest, cookieAgentFactory OK = 'ok' +def _auth_required(func): + """ + Decorate a method so that it will not be called if the instance + attribute `is_authenticated` does not evaluate to True. + """ + def decorated(*args, **kwargs): + instance = args[0] + allowed = getattr(instance, 'is_authenticated') + if not allowed: + raise RuntimeError('This method requires authentication') + return func(*args, **kwargs) + return decorated + + class Session(object): def __init__(self, credentials, api, provider_cert): @@ -101,7 +114,7 @@ class Session(object): self._token = token defer.returnValue(OK) - @auth_required + @_auth_required @defer.inlineCallbacks def logout(self): uri = self._api.get_logout_uri() @@ -121,7 +134,7 @@ class Session(object): met = self._api.get_vpn_cert_method() return self._request(self._agent, uri, method=met) - @auth_required + @_auth_required def get_smtp_cert(self): # TODO pass it to the provider object so that it can save it in the # right path. @@ -152,8 +165,9 @@ class Session(object): self.password = password defer.returnValue((OK, registered_user)) - @auth_required + @_auth_required def update_user_record(self): + # FIXME to be implemented pass # Authentication-protected configuration -- cgit v1.2.3 From ee5033a6a0ce59eaa9e20aea913b6cd710c80ccd Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 00:56:05 -0400 Subject: fix hardcoded ca path --- bonafide/src/leap/bonafide/_protocol.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index e66be19..6a5c8b6 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -24,6 +24,7 @@ from collections import defaultdict from leap.bonafide import config from leap.bonafide import provider from leap.bonafide.session import Session, OK +from leap.common.config import get_path_prefix from twisted.cred.credentials import UsernamePassword from twisted.internet.defer import fail @@ -34,6 +35,7 @@ from twisted.python import log # TODO [ ] read provider info COMMANDS = 'signup', 'authenticate', 'logout', 'stats' +_preffix = get_path_prefix() class BonafideProtocol(object): @@ -64,10 +66,8 @@ class BonafideProtocol(object): username, provider_id = config.get_username_and_provider(full_id) credentials = UsernamePassword(username, password) api = self._get_api(provider_id) - cdev_pem = os.path.expanduser( - '~/.config/leap/providers/%s/keys/ca/cacert.pem' % - provider_id) - session = Session(credentials, api, cdev_pem) + provider_pem = _get_provider_ca_path(provider_id) + session = Session(credentials, api, provider_pem) self._sessions[full_id] = session return session @@ -160,3 +160,8 @@ class BonafideProtocol(object): mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( len(self._sessions), mem / 1024) + + +def _get_provider_ca_path(provider_id): + return os.path.join( + _preffix, 'leap', 'providers', provider_id, 'keys', 'ca', 'cacert.pem') -- cgit v1.2.3 From 97126d0858d8cb5e6e419666160a6818cb64b74e Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 00:58:01 -0400 Subject: fix extra debug line --- bonafide/src/leap/bonafide/_protocol.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index 6a5c8b6..e58fbf4 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -97,7 +97,6 @@ class BonafideProtocol(object): return d def do_authenticate(self, full_id, password): - log.msg('SIGNUP for %s' % full_id) _, provider_id = config.get_username_and_provider(full_id) provider = config.Provider(provider_id) -- cgit v1.2.3 From 242aaa656339c020381083593122ebbfef7141c6 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 01:00:36 -0400 Subject: remove unneeded try/except --- bonafide/src/leap/bonafide/_protocol.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index e58fbf4..e9c60de 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -133,12 +133,8 @@ class BonafideProtocol(object): session = self._get_session(full_id) if not session.is_authenticated: return fail(RuntimeError("There is no session for such user")) - try: - d = session.logout() - except Exception as exc: - log.err(exc) - return fail(exc) + d = session.logout() d.addCallback(lambda _: self._sessions.pop(full_id)) d.addCallback(lambda _: '%s logged out' % full_id) return d -- cgit v1.2.3 From 4cb031e3de822e95d1d16105e786329c686b91c4 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 01:03:13 -0400 Subject: add a couple of tbd --- bonafide/src/leap/bonafide/_protocol.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index e9c60de..b15c6ac 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -145,16 +145,18 @@ class BonafideProtocol(object): return d def do_get_vpn_cert(self): + # FIXME to be implemented pass def do_update_user(self): + # FIXME to be implemented pass def do_stats(self): log.msg('Calculating Bonafide Service STATS') mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss - return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( - len(self._sessions), mem / 1024) + return {'sessions': len(self._sessions), + 'mem': '%s KB' % (mem / 1024)} def _get_provider_ca_path(provider_id): -- cgit v1.2.3 From 05a15eb66ce7912094361e78e81d073e88cae21b Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 01:26:58 -0400 Subject: hide srpauth implementation details --- bonafide/src/leap/bonafide/_srp.py | 48 +++++++++++++++++++---------------- bonafide/src/leap/bonafide/session.py | 25 ++++++++---------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/bonafide/src/leap/bonafide/_srp.py b/bonafide/src/leap/bonafide/_srp.py index 1c711f3..38f657b 100644 --- a/bonafide/src/leap/bonafide/_srp.py +++ b/bonafide/src/leap/bonafide/_srp.py @@ -31,41 +31,45 @@ class SRPAuthMechanism(object): Implement a protocol-agnostic SRP Authentication mechanism. """ - def initialize(self, username, password): - srp_user = srp.User(username, password, - srp.SHA256, srp.NG_1024) - _, A = srp_user.start_authentication() - return srp_user, A - - def get_handshake_params(self, username, A): - return {'login': bytes(username), 'A': binascii.hexlify(A)} - - def process_handshake(self, srp_user, handshake_response): + def __init__(self, username, password): + self.username = username + self.srp_user = srp.User(username, password, + srp.SHA256, srp.NG_1024) + _, A = self.srp_user.start_authentication() + self.A = A + self.M = None + self.M2 = None + + def get_handshake_params(self): + return {'login': bytes(self.username), + 'A': binascii.hexlify(self.A)} + + def process_handshake(self, handshake_response): challenge = json.loads(handshake_response) self._check_for_errors(challenge) salt = challenge.get('salt', None) B = challenge.get('B', None) unhex_salt, unhex_B = self._unhex_salt_B(salt, B) - M = srp_user.process_challenge(unhex_salt, unhex_B) - return M + self.M = self.srp_user.process_challenge(unhex_salt, unhex_B) - def get_authentication_params(self, M, A): + def get_authentication_params(self): # It looks A is not used server side - return {'client_auth': binascii.hexlify(M), 'A': binascii.hexlify(A)} + return {'client_auth': binascii.hexlify(self.M), + 'A': binascii.hexlify(self.A)} def process_authentication(self, authentication_response): auth = json.loads(authentication_response) self._check_for_errors(auth) uuid = auth.get('id', None) token = auth.get('token', None) - M2 = auth.get('M2', None) - self._check_auth_params(uuid, token, M2) - return uuid, token, M2 - - def verify_authentication(self, srp_user, M2): - unhex_M2 = _safe_unhexlify(M2) - srp_user.verify_session(unhex_M2) - assert srp_user.authenticated() + self.M2 = auth.get('M2', None) + self._check_auth_params(uuid, token, self.M2) + return uuid, token + + def verify_authentication(self): + unhex_M2 = _safe_unhexlify(self.M2) + self.srp_user.verify_session(unhex_M2) + assert self.srp_user.authenticated() def _check_for_errors(self, response): if 'errors' in response: diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index 547f0dd..ec1587f 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -61,9 +61,10 @@ class Session(object): def _initialize_session(self): self._agent = cookieAgentFactory(self._provider_cert) - self._srp_auth = _srp.SRPAuthMechanism() + username = self.username or '' + password = self.password or '' + self._srp_auth = _srp.SRPAuthMechanism(username, password) self._srp_signup = _srp.SRPSignupMechanism() - self._srp_user = None self._token = None self._uuid = None @@ -79,36 +80,30 @@ class Session(object): @property def is_authenticated(self): - if not self._srp_user: - return False - return self._srp_user.authenticated() + return self._srp_auth.srp_user.authenticated() @defer.inlineCallbacks def authenticate(self): - srpuser, A = self._srp_auth.initialize( - self.username, self.password) - self._srp_user = srpuser - uri = self._api.get_handshake_uri() met = self._api.get_handshake_method() log.msg("%s to %s" % (met, uri)) - params = self._srp_auth.get_handshake_params(self.username, A) + params = self._srp_auth.get_handshake_params() handshake = yield self._request(self._agent, uri, values=params, method=met) - M = self._srp_auth.process_handshake(srpuser, handshake) + self._srp_auth.process_handshake(handshake) uri = self._api.get_authenticate_uri(login=self.username) met = self._api.get_authenticate_method() log.msg("%s to %s" % (met, uri)) - params = self._srp_auth.get_authentication_params(M, A) + params = self._srp_auth.get_authentication_params() auth = yield self._request(self._agent, uri, values=params, method=met) - uuid, token, M2 = self._srp_auth.process_authentication(auth) - self._srp_auth.verify_authentication(srpuser, M2) + uuid, token = self._srp_auth.process_authentication(auth) + self._srp_auth.verify_authentication() self._uuid = uuid self._token = token @@ -120,6 +115,8 @@ class Session(object): uri = self._api.get_logout_uri() met = self._api.get_logout_method() auth = yield self._request(self._agent, uri, method=met) + print 'AUTH', auth + print 'resetting user/pass' self.username = None self.password = None self._initialize_session() -- cgit v1.2.3 From 38a5c3c73832110922b3628e791070d2034c625c Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 01:29:26 -0400 Subject: reorder class for readability --- bonafide/src/leap/bonafide/config.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index f866368..5aba663 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -124,11 +124,6 @@ def get_username_and_provider(full_id): return full_id.split('@') -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) - - class Provider(object): # TODO add validation @@ -313,7 +308,8 @@ class Provider(object): # FIXME --- configs.json raises 500, see #7914. # This is a workaround until that's fixed. log.err(failure) - log.msg("COULD NOT VERIFY CONFIGS.JSON, WORKAROUND: DIRECT DOWNLOAD") + log.msg( + "COULD NOT VERIFY CONFIGS.JSON, WORKAROUND: DIRECT DOWNLOAD") if 'mx' in self._provider_config.services: soledad_uri = '/1/config/soledad-service.json' @@ -323,8 +319,10 @@ class Provider(object): fetch = self._fetch_provider_configs_unauthenticated get_path = self._get_service_config_path - d1 = fetch('https://' + str(base + soledad_uri), get_path('soledad')) - d2 = fetch('https://' + str(base + smtp_uri), get_path('smtp')) + d1 = fetch( + 'https://' + str(base + soledad_uri), get_path('soledad')) + d2 = fetch( + 'https://' + str(base + smtp_uri), get_path('smtp')) d = defer.gatherResults([d1, d2]) d.addCallback(lambda _: finish_stuck_after_workaround()) return d @@ -479,6 +477,11 @@ class Record(object): self.__dict__.update(kw) +class WebClientContextFactory(ClientContextFactory): + def getContext(self, hostname, port): + return ClientContextFactory.getContext(self) + + if __name__ == '__main__': def print_done(): -- cgit v1.2.3 From ed3a66e70899231ae90ba323ee82f52af027c26c Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 01:42:45 -0400 Subject: fix hardcoded basedir path --- bonafide/src/leap/bonafide/config.py | 9 +++++++-- bonafide/src/leap/bonafide/service.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index 5aba663..496c9a8 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -47,6 +47,9 @@ def get_path_prefix(standalone=False): return common_get_path_prefix(standalone) +_preffix = get_path_prefix() + + def get_provider_path(domain, config='provider.json'): """ Returns relative path for provider configs. @@ -135,10 +138,12 @@ class Provider(object): ongoing_bootstrap = defaultdict(None) stuck_bootstrap = defaultdict(None) - def __init__(self, domain, autoconf=True, basedir='~/.config/leap', + def __init__(self, domain, autoconf=True, basedir=None, check_certificate=True): - self._domain = domain + if not basedir: + basedir = os.path.join(_preffix, 'leap') self._basedir = os.path.expanduser(basedir) + self._domain = domain self._disco = Discovery('https://%s' % domain) self._provider_config = None diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 87c2ef5..6d3c61e 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -21,6 +21,7 @@ Bonafide Service. import os from collections import defaultdict +from leap.common.config import get_path_prefix from leap.common.service_hooks import HookableService from leap.bonafide._protocol import BonafideProtocol @@ -29,12 +30,16 @@ from twisted.internet import defer from twisted.python import log +_preffix = get_path_prefix() + + class BonafideService(service.Service, HookableService): - def __init__(self, basedir='~/.config/leap'): - # TODO fix hardcoded basedir - self._bonafide = BonafideProtocol() + def __init__(self, basedir=None): + if not basedir: + basedir = os.path.join(_preffix, 'leap') self._basedir = os.path.expanduser(basedir) + self._bonafide = BonafideProtocol() self.service_hooks = defaultdict(list) # XXX this is a quick hack to get a ref -- cgit v1.2.3 From a40da9da04afde32d9ab76a9d04bbf1dd0cd8d87 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 01:52:51 -0400 Subject: add TODO about possible refactor --- bonafide/src/leap/bonafide/session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py index ec1587f..4180041 100644 --- a/bonafide/src/leap/bonafide/session.py +++ b/bonafide/src/leap/bonafide/session.py @@ -52,6 +52,7 @@ class Session(object): # this provider can have an api attribute, # and a "autoconfig" attribute passed on initialization. # TODO get a file-descriptor for password if not in credentials + # TODO merge self._request with config.Provider._http_request ? self.username = credentials.username self.password = credentials.password -- cgit v1.2.3 From be9b500825fbe9061997c0722e659cd6deac67e2 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Fri, 4 Mar 2016 11:03:30 -0400 Subject: update README --- bonafide/README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bonafide/README.rst b/bonafide/README.rst index 554bb37..7a7b20e 100644 --- a/bonafide/README.rst +++ b/bonafide/README.rst @@ -18,10 +18,10 @@ the package in development mode by running:: from the parent folder. -To run the bonafide service, you can use the bitmask.core daemon. From the root -of the bitmask_core repo:: +To run the bonafide service, you can use the bitmask.core daemon. From an +environment in which you have installed the bitmask_core repo:: - make bitmaskd + bitmaskd Then you can use `bitmask_cli` to see the available actions, under the user command. -- cgit v1.2.3 From c6dc131c371ceba1e21ce963aeedf04aa619f3c4 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Wed, 27 Apr 2016 21:51:34 -0400 Subject: [pkg] do not import unused twisted Service --- bonafide/src/leap/bonafide/service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 6d3c61e..38c54e1 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -25,7 +25,6 @@ from leap.common.config import get_path_prefix from leap.common.service_hooks import HookableService from leap.bonafide._protocol import BonafideProtocol -from twisted.application import service from twisted.internet import defer from twisted.python import log @@ -33,7 +32,7 @@ from twisted.python import log _preffix = get_path_prefix() -class BonafideService(service.Service, HookableService): +class BonafideService(HookableService): def __init__(self, basedir=None): if not basedir: -- cgit v1.2.3 From 78de4ce3aada7e5c40880175544d679f0228a670 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 19 May 2016 12:27:36 +0200 Subject: [feat] remove password from logout Logout does not need password. --- bonafide/src/leap/bonafide/_protocol.py | 2 +- bonafide/src/leap/bonafide/service.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index b15c6ac..9e964af 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -127,7 +127,7 @@ class BonafideProtocol(object): d.addCallback(return_token_and_uuid, session) return d - def do_logout(self, full_id, password): + def do_logout(self, full_id): # XXX use the AVATAR here log.msg('LOGOUT for %s' % full_id) session = self._get_session(full_id) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 38c54e1..4d5ee78 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -89,7 +89,7 @@ class BonafideService(HookableService): d.addCallback(lambda response: {'signup': 'ok', 'user': response}) return d - def do_logout(self, username, password): + def do_logout(self, username): if not username: username = self._active_user @@ -97,7 +97,7 @@ class BonafideService(HookableService): self._active_user = None return passthrough - d = self._bonafide.do_logout(username, password) + d = self._bonafide.do_logout(username) d.addCallback(reset_active) d.addCallback(lambda response: {'logout': 'ok'}) return d -- cgit v1.2.3 From fff8b5be74105d95a8b556e06b1e99cd7743749b Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 19 May 2016 13:32:43 +0200 Subject: [bug] raise exception on authentiation errors To avoid using None as a valid (token, uuid) pair we need to raise an exception. --- bonafide/src/leap/bonafide/service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index 4d5ee78..ff3c30a 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -59,8 +59,9 @@ class BonafideService(HookableService): def notify_bonafide_auth(result): if not result: - log.msg("Authentication hook did not return anything") - return + msg = "Authentication hook did not return anything" + log.msg(msg) + raise RuntimeError(msg) token, uuid = result data = dict(username=username, token=token, uuid=uuid, -- cgit v1.2.3 From a2adce4619f9cc946301deede86871a4fada6143 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Sat, 30 Apr 2016 14:23:04 -0300 Subject: [feat] use the api_uri and api_version from the provider.json --- bonafide/src/leap/bonafide/_protocol.py | 45 ++++++++++++++++++--------------- bonafide/src/leap/bonafide/config.py | 19 +++++++++++--- bonafide/src/leap/bonafide/provider.py | 9 +++++++ 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index 9e964af..e50d020 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -22,7 +22,7 @@ import resource from collections import defaultdict from leap.bonafide import config -from leap.bonafide import provider +from leap.bonafide.provider import Api from leap.bonafide.session import Session, OK from leap.common.config import get_path_prefix @@ -46,18 +46,17 @@ class BonafideProtocol(object): _apis = defaultdict(None) _sessions = defaultdict(None) - def _get_api(self, provider_id): + def _get_api(self, provider): # TODO should get deferred - if provider_id in self._apis: - return self._apis[provider_id] + if provider.domain in self._apis: + return self._apis[provider.domain] - # XXX lookup the provider config instead # TODO defer the autoconfig for the provider if needed... - api = provider.Api('https://api.%s:4430' % provider_id) - self._apis[provider_id] = api + api = Api(provider.api_uri, provider.version) + self._apis[provider.domain] = api return api - def _get_session(self, full_id, password=""): + def _get_session(self, provider, full_id, password=""): if full_id in self._sessions: return self._sessions[full_id] @@ -65,7 +64,7 @@ class BonafideProtocol(object): # TODO use twisted.cred instead username, provider_id = config.get_username_and_provider(full_id) credentials = UsernamePassword(username, password) - api = self._get_api(provider_id) + api = self._get_api(provider) provider_pem = _get_provider_ca_path(provider_id) session = Session(credentials, api, provider_pem) self._sessions[full_id] = session @@ -78,10 +77,11 @@ class BonafideProtocol(object): _, provider_id = config.get_username_and_provider(full_id) provider = config.Provider(provider_id) - d = provider.callWhenReady(self._do_signup, full_id, password) + d = provider.callWhenReady( + self._do_signup, provider, full_id, password) return d - def _do_signup(self, full_id, password): + def _do_signup(self, provider, full_id, password): # XXX check it's unauthenticated def return_user(result, _session): @@ -91,7 +91,7 @@ class BonafideProtocol(object): username, _ = config.get_username_and_provider(full_id) # XXX get deferred? - session = self._get_session(full_id, password) + session = self._get_session(provider, full_id, password) d = session.signup(username, password) d.addCallback(return_user, session) return d @@ -102,17 +102,17 @@ class BonafideProtocol(object): provider = config.Provider(provider_id) def maybe_finish_provider_bootstrap(result, provider): - session = self._get_session(full_id, password) + session = self._get_session(provider, full_id, password) d = provider.download_services_config_with_auth(session) d.addCallback(lambda _: result) return d - d = provider.callWhenMainConfigReady( - self._do_authenticate, full_id, password) + d = provider.callWhenReady( + self._do_authenticate, provider, full_id, password) d.addCallback(maybe_finish_provider_bootstrap, provider) return d - def _do_authenticate(self, full_id, password): + def _do_authenticate(self, provider, full_id, password): def return_token_and_uuid(result, _session): if result == OK: @@ -122,7 +122,7 @@ class BonafideProtocol(object): log.msg('AUTH for %s' % full_id) # XXX get deferred? - session = self._get_session(full_id, password) + session = self._get_session(provider, full_id, password) d = session.authenticate() d.addCallback(return_token_and_uuid, session) return d @@ -130,9 +130,10 @@ class BonafideProtocol(object): def do_logout(self, full_id): # XXX use the AVATAR here log.msg('LOGOUT for %s' % full_id) - session = self._get_session(full_id) - if not session.is_authenticated: + if (full_id not in self._sessions or + not self._sessions[full_id].is_authenticated): return fail(RuntimeError("There is no session for such user")) + session = self._sessions[full_id] d = session.logout() d.addCallback(lambda _: self._sessions.pop(full_id)) @@ -140,8 +141,10 @@ class BonafideProtocol(object): return d def do_get_smtp_cert(self, full_id): - session = self._get_session(full_id) - d = session.get_smtp_cert() + if (full_id not in self._sessions or + not self._sessions[full_id].is_authenticated): + return fail(RuntimeError("There is no session for such user")) + d = self._sessions[full_id].get_smtp_cert() return d def do_get_vpn_cert(self): diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index 496c9a8..8c34bc1 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -174,6 +174,22 @@ class Provider(object): self.ongoing_bootstrap[self._domain] = defer.succeed( 'already_initialized') + @property + def domain(self): + return self._domain + + @property + def api_uri(self): + if not self._provider_config: + return 'https://api.%s:4430' % self._domain + return self._provider_config.api_uri + + @property + def version(self): + if not self._provider_config: + return 1 + return int(self._provider_config.api_version) + def is_configured(self): provider_json = self._get_provider_json_path() # XXX check if all the services are there @@ -473,9 +489,6 @@ class Provider(object): # XXX pass if-modified-since header return httpRequest(self._agent, *args, **kw) - def _get_api_uri(self): - pass - class Record(object): def __init__(self, **kw): diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py index 7e78196..82824e9 100644 --- a/bonafide/src/leap/bonafide/provider.py +++ b/bonafide/src/leap/bonafide/provider.py @@ -24,6 +24,12 @@ import re from urlparse import urlparse +""" +Maximum API version number supported by bonafide +""" +MAX_API_VERSION = 1 + + class _MetaActionDispatcher(type): """ @@ -84,7 +90,10 @@ class BaseProvider(object): raise ValueError( 'ProviderApi needs to be passed a url with https scheme') self.netloc = parsed.netloc + self.version = version + if version > MAX_API_VERSION: + self.version = MAX_API_VERSION def get_hostname(self): return urlparse(self._get_base_url()).hostname -- cgit v1.2.3 From 1206c054753e25d2c3d542cefdb2adca823c09a1 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Sat, 30 Apr 2016 15:46:29 -0300 Subject: [feat] use netloc instead of an extra api_uri in Discovery --- bonafide/src/leap/bonafide/config.py | 2 +- bonafide/src/leap/bonafide/provider.py | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index 8c34bc1..9da0557 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -452,7 +452,7 @@ class Provider(object): api_uri = self._provider_config.api_uri if api_uri: parsed = urlparse(api_uri) - self._disco.api_uri = parsed.netloc + self._disco.netloc = parsed.netloc def _get_config_for_all_services(self, session): services_dict = self._load_provider_configs() diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py index 82824e9..b44dae2 100644 --- a/bonafide/src/leap/bonafide/provider.py +++ b/bonafide/src/leap/bonafide/provider.py @@ -160,19 +160,8 @@ class Discovery(BaseProvider): 'configs': ('1/configs.json', 'GET'), } - api_uri = None - api_port = None - def _get_base_url(self): - if self.api_uri: - base = self.api_uri - else: - base = self.netloc - - uri = "https://{0}".format(base) - if self.api_port: - uri = uri + ':%s' % self.api_port - return uri + return "https://{0}".format(self.netloc) def get_base_uri(self): return self._get_base_url() -- cgit v1.2.3 From cc640c9b05aa6f74c7610bfa141b6f2c631ff9b8 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 20 Jun 2016 20:48:01 +0200 Subject: [bug] the api_uri in Dicovery is called netloc --- bonafide/src/leap/bonafide/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index 9da0557..c3097cc 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -335,7 +335,7 @@ class Provider(object): if 'mx' in self._provider_config.services: soledad_uri = '/1/config/soledad-service.json' smtp_uri = '/1/config/smtp-service.json' - base = self._disco.api_uri + base = self._disco.netloc fetch = self._fetch_provider_configs_unauthenticated get_path = self._get_service_config_path -- cgit v1.2.3 From b97643c5b8cbee752659269f0be15ff460efe193 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Sat, 27 Aug 2016 18:43:15 +0200 Subject: [bug] let the failures propagate In the case of wrong username/password the failure returned was 'Authentication hook did not return anything'. Right now the original failure is returned, and the producer of the failure can put a more meaningful message. - Resolves: #8399 --- bonafide/src/leap/bonafide/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py index c3097cc..ae66a0e 100644 --- a/bonafide/src/leap/bonafide/config.py +++ b/bonafide/src/leap/bonafide/config.py @@ -227,13 +227,11 @@ class Provider(object): def callWhenMainConfigReady(self, cb, *args, **kw): d = self.first_bootstrap[self._domain] d.addCallback(lambda _: cb(*args, **kw)) - d.addErrback(log.err) return d def callWhenReady(self, cb, *args, **kw): d = self.ongoing_bootstrap[self._domain] d.addCallback(lambda _: cb(*args, **kw)) - d.addErrback(log.err) return d def has_valid_certificate(self): -- cgit v1.2.3 From ec86a9c330fd6c183563ca3b2c5b6b2068bb0e3b Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Sat, 27 Aug 2016 19:14:54 +0200 Subject: [bug] don't cache failed sessions We were caching sessions even when the authentication has failed, making impossible to try again with a different password. - Resolves: #8226 --- bonafide/src/leap/bonafide/_protocol.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py index e50d020..726185e 100644 --- a/bonafide/src/leap/bonafide/_protocol.py +++ b/bonafide/src/leap/bonafide/_protocol.py @@ -70,6 +70,11 @@ class BonafideProtocol(object): self._sessions[full_id] = session return session + def _del_session_errback(self, failure, full_id): + if full_id in self._sessions: + del self._sessions[full_id] + return failure + # Service public methods def do_signup(self, full_id, password): @@ -94,6 +99,7 @@ class BonafideProtocol(object): session = self._get_session(provider, full_id, password) d = session.signup(username, password) d.addCallback(return_user, session) + d.addErrback(self._del_session_errback, full_id) return d def do_authenticate(self, full_id, password): @@ -125,6 +131,7 @@ class BonafideProtocol(object): session = self._get_session(provider, full_id, password) d = session.authenticate() d.addCallback(return_token_and_uuid, session) + d.addErrback(self._del_session_errback, full_id) return d def do_logout(self, full_id): -- cgit v1.2.3 From 15394a9cefdf6f8ca9cbb6604c6f68425577ae90 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Mon, 29 Aug 2016 12:42:51 -0400 Subject: [bug] return active user as a dict since the command dispatcher expects dicts now --- bonafide/src/leap/bonafide/service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py index ff3c30a..14585ef 100644 --- a/bonafide/src/leap/bonafide/service.py +++ b/bonafide/src/leap/bonafide/service.py @@ -115,5 +115,6 @@ class BonafideService(HookableService): return d def do_get_active_user(self): - user = self._active_user - return defer.succeed(user) + user = self._active_user or '' + info = {'user': user} + return defer.succeed(info) -- cgit v1.2.3