From aa882b7a64f2c821c8e241a5ff725f27c64fc15f Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Mon, 29 Aug 2016 23:00:25 -0400 Subject: [pkg] move bonafide source to leap.bitmask.bonafide --- bonafide/.gitattributes | 1 - bonafide/.gitignore | 11 - bonafide/AUTHORS | 0 bonafide/CHANGELOG | 0 bonafide/LICENSE | 619 --------- bonafide/MANIFEST.in | 2 - bonafide/Makefile | 2 - bonafide/README.rst | 27 - bonafide/pkg/__init__.py | 0 bonafide/pkg/requirements-leap.pip | 1 - bonafide/pkg/requirements.pip | 4 - bonafide/pkg/utils.py | 84 -- bonafide/setup.cfg | 17 - bonafide/setup.py | 90 -- bonafide/src/leap/__init__.py | 6 - bonafide/src/leap/bonafide/__init__.py | 4 - bonafide/src/leap/bonafide/_http.py | 95 -- bonafide/src/leap/bonafide/_protocol.py | 174 --- bonafide/src/leap/bonafide/_srp.py | 147 -- bonafide/src/leap/bonafide/_version.py | 460 ------ bonafide/src/leap/bonafide/bootstrap.py | 0 bonafide/src/leap/bonafide/config.py | 508 ------- bonafide/src/leap/bonafide/cred_srp.py | 157 --- bonafide/src/leap/bonafide/provider.py | 186 --- bonafide/src/leap/bonafide/service.py | 120 -- 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 | 219 --- bonafide/src/leap/bonafide/ssh_service.py | 2 - bonafide/src/leap/bonafide/tests/__init__.py | 0 bonafide/versioneer.py | 1699 ----------------------- src/leap/bitmask/bonafide/__init__.py | 4 + src/leap/bitmask/bonafide/_http.py | 95 ++ src/leap/bitmask/bonafide/_protocol.py | 174 +++ src/leap/bitmask/bonafide/_srp.py | 147 ++ src/leap/bitmask/bonafide/_version.py | 460 ++++++ src/leap/bitmask/bonafide/bootstrap.py | 0 src/leap/bitmask/bonafide/config.py | 508 +++++++ src/leap/bitmask/bonafide/cred_srp.py | 157 +++ src/leap/bitmask/bonafide/provider.py | 186 +++ src/leap/bitmask/bonafide/service.py | 120 ++ src/leap/bitmask/bonafide/services/TODO | 1 + src/leap/bitmask/bonafide/services/__init__.py | 0 src/leap/bitmask/bonafide/services/eip.py | 0 src/leap/bitmask/bonafide/services/mail.py | 0 src/leap/bitmask/bonafide/services/soledad.py | 0 src/leap/bitmask/bonafide/session.py | 219 +++ src/leap/bitmask/bonafide/ssh_service.py | 2 + src/leap/bitmask/bonafide/tests/__init__.py | 0 52 files changed, 2073 insertions(+), 4636 deletions(-) delete mode 100644 bonafide/.gitattributes delete mode 100644 bonafide/.gitignore delete mode 100644 bonafide/AUTHORS delete mode 100644 bonafide/CHANGELOG delete mode 100644 bonafide/LICENSE delete mode 100644 bonafide/MANIFEST.in delete mode 100644 bonafide/Makefile delete mode 100644 bonafide/README.rst delete mode 100644 bonafide/pkg/__init__.py delete mode 100644 bonafide/pkg/requirements-leap.pip delete mode 100644 bonafide/pkg/requirements.pip delete mode 100644 bonafide/pkg/utils.py delete mode 100644 bonafide/setup.cfg delete mode 100644 bonafide/setup.py delete mode 100644 bonafide/src/leap/__init__.py delete mode 100644 bonafide/src/leap/bonafide/__init__.py delete mode 100644 bonafide/src/leap/bonafide/_http.py delete mode 100644 bonafide/src/leap/bonafide/_protocol.py delete mode 100644 bonafide/src/leap/bonafide/_srp.py delete mode 100644 bonafide/src/leap/bonafide/_version.py delete mode 100644 bonafide/src/leap/bonafide/bootstrap.py delete mode 100644 bonafide/src/leap/bonafide/config.py delete mode 100644 bonafide/src/leap/bonafide/cred_srp.py delete mode 100644 bonafide/src/leap/bonafide/provider.py delete mode 100644 bonafide/src/leap/bonafide/service.py delete mode 100644 bonafide/src/leap/bonafide/services/TODO delete mode 100644 bonafide/src/leap/bonafide/services/__init__.py delete mode 100644 bonafide/src/leap/bonafide/services/eip.py delete mode 100644 bonafide/src/leap/bonafide/services/mail.py delete mode 100644 bonafide/src/leap/bonafide/services/soledad.py delete mode 100644 bonafide/src/leap/bonafide/session.py delete mode 100644 bonafide/src/leap/bonafide/ssh_service.py delete mode 100644 bonafide/src/leap/bonafide/tests/__init__.py delete mode 100644 bonafide/versioneer.py create mode 100644 src/leap/bitmask/bonafide/__init__.py create mode 100644 src/leap/bitmask/bonafide/_http.py create mode 100644 src/leap/bitmask/bonafide/_protocol.py create mode 100644 src/leap/bitmask/bonafide/_srp.py create mode 100644 src/leap/bitmask/bonafide/_version.py create mode 100644 src/leap/bitmask/bonafide/bootstrap.py create mode 100644 src/leap/bitmask/bonafide/config.py create mode 100644 src/leap/bitmask/bonafide/cred_srp.py create mode 100644 src/leap/bitmask/bonafide/provider.py create mode 100644 src/leap/bitmask/bonafide/service.py create mode 100644 src/leap/bitmask/bonafide/services/TODO create mode 100644 src/leap/bitmask/bonafide/services/__init__.py create mode 100644 src/leap/bitmask/bonafide/services/eip.py create mode 100644 src/leap/bitmask/bonafide/services/mail.py create mode 100644 src/leap/bitmask/bonafide/services/soledad.py create mode 100644 src/leap/bitmask/bonafide/session.py create mode 100644 src/leap/bitmask/bonafide/ssh_service.py create mode 100644 src/leap/bitmask/bonafide/tests/__init__.py diff --git a/bonafide/.gitattributes b/bonafide/.gitattributes deleted file mode 100644 index 1dc749d3..00000000 --- a/bonafide/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -src/leap/bonafide/_version.py export-subst diff --git a/bonafide/.gitignore b/bonafide/.gitignore deleted file mode 100644 index 6fbbf52d..00000000 --- a/bonafide/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -*.pyc -*.egg -*.egg-info -*.swp -*.swo -.project -*.pid -dist/ -build/ -MANIFEST -_trial_temp diff --git a/bonafide/AUTHORS b/bonafide/AUTHORS deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/CHANGELOG b/bonafide/CHANGELOG deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/LICENSE b/bonafide/LICENSE deleted file mode 100644 index bc08fe2e..00000000 --- a/bonafide/LICENSE +++ /dev/null @@ -1,619 +0,0 @@ - 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/MANIFEST.in b/bonafide/MANIFEST.in deleted file mode 100644 index 3355eddc..00000000 --- a/bonafide/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include versioneer.py -include src/leap/bonafide/_version.py diff --git a/bonafide/Makefile b/bonafide/Makefile deleted file mode 100644 index c3110055..00000000 --- a/bonafide/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -bonafide_server: - twistd -n -y src/leap/bonafide/zmq.tac diff --git a/bonafide/README.rst b/bonafide/README.rst deleted file mode 100644 index 7a7b20e9..00000000 --- a/bonafide/README.rst +++ /dev/null @@ -1,27 +0,0 @@ -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. 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 - -Using ------ - -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. - -To run the bonafide service, you can use the bitmask.core daemon. From an -environment in which you have installed the bitmask_core repo:: - - bitmaskd - -Then you can use `bitmask_cli` to see the available actions, under the user -command. diff --git a/bonafide/pkg/__init__.py b/bonafide/pkg/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/pkg/requirements-leap.pip b/bonafide/pkg/requirements-leap.pip deleted file mode 100644 index 72d41e47..00000000 --- a/bonafide/pkg/requirements-leap.pip +++ /dev/null @@ -1 +0,0 @@ -leap.common diff --git a/bonafide/pkg/requirements.pip b/bonafide/pkg/requirements.pip deleted file mode 100644 index a8c4ffec..00000000 --- a/bonafide/pkg/requirements.pip +++ /dev/null @@ -1,4 +0,0 @@ -srp -twisted>=14.0.0 -service-identity -colorama diff --git a/bonafide/pkg/utils.py b/bonafide/pkg/utils.py deleted file mode 100644 index 521cd4ed..00000000 --- a/bonafide/pkg/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- 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 deleted file mode 100644 index edf2450d..00000000 --- a/bonafide/setup.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[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 - -[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 deleted file mode 100644 index 154ac7c7..00000000 --- a/bonafide/setup.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- 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 - -import versioneer - -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') - -VERSION = versioneer.get_version() -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 = "" - -bonafide_cli = 'bonafide_cli=leap.bonafide.bonafide_cli:main' - -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, - entry_points={ - 'console_scripts': [bonafide_cli] - }, -) diff --git a/bonafide/src/leap/__init__.py b/bonafide/src/leap/__init__.py deleted file mode 100644 index f48ad105..00000000 --- a/bonafide/src/leap/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# 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 deleted file mode 100644 index 74f4e668..00000000 --- a/bonafide/src/leap/bonafide/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions diff --git a/bonafide/src/leap/bonafide/_http.py b/bonafide/src/leap/bonafide/_http.py deleted file mode 100644 index 8f05b421..00000000 --- a/bonafide/src/leap/bonafide/_http.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- 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 base64 -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', 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): - # print "RESPONSE CODE", response.code - 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/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py deleted file mode 100644 index 726185ea..00000000 --- a/bonafide/src/leap/bonafide/_protocol.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- 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.provider import Api -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 -from twisted.python import log - - -# TODO [ ] enable-disable services -# TODO [ ] read provider info - -COMMANDS = 'signup', 'authenticate', 'logout', 'stats' -_preffix = get_path_prefix() - - -class BonafideProtocol(object): - """ - Expose the protocol that interacts with the Bonafide Service API. - """ - - _apis = defaultdict(None) - _sessions = defaultdict(None) - - def _get_api(self, provider): - # TODO should get deferred - if provider.domain in self._apis: - return self._apis[provider.domain] - - # TODO defer the autoconfig for the provider if needed... - api = Api(provider.api_uri, provider.version) - self._apis[provider.domain] = api - return api - - def _get_session(self, provider, 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) - provider_pem = _get_provider_ca_path(provider_id) - session = Session(credentials, api, provider_pem) - 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): - 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, provider, full_id, password) - return d - - def _do_signup(self, provider, full_id, password): - - # XXX check it's unauthenticated - def return_user(result, _session): - return_code, user = result - if return_code == OK: - return user - - username, _ = config.get_username_and_provider(full_id) - # XXX get deferred? - 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): - _, provider_id = config.get_username_and_provider(full_id) - - provider = config.Provider(provider_id) - - def maybe_finish_provider_bootstrap(result, provider): - session = self._get_session(provider, full_id, password) - d = provider.download_services_config_with_auth(session) - d.addCallback(lambda _: result) - return d - - d = provider.callWhenReady( - self._do_authenticate, provider, full_id, password) - d.addCallback(maybe_finish_provider_bootstrap, provider) - return d - - def _do_authenticate(self, provider, 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) - - # XXX get deferred? - 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): - # XXX use the AVATAR here - log.msg('LOGOUT for %s' % full_id) - 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)) - d.addCallback(lambda _: '%s logged out' % full_id) - return d - - def do_get_smtp_cert(self, full_id): - 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): - # 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 {'sessions': len(self._sessions), - 'mem': '%s KB' % (mem / 1024)} - - -def _get_provider_ca_path(provider_id): - return os.path.join( - _preffix, 'leap', 'providers', provider_id, 'keys', 'ca', 'cacert.pem') diff --git a/bonafide/src/leap/bonafide/_srp.py b/bonafide/src/leap/bonafide/_srp.py deleted file mode 100644 index 38f657b3..00000000 --- a/bonafide/src/leap/bonafide/_srp.py +++ /dev/null @@ -1,147 +0,0 @@ -# -*- coding: utf-8 -*- -# _srp.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 __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) - self.M = self.srp_user.process_challenge(unhex_salt, unhex_B) - - def get_authentication_params(self): - # It looks A is not used server side - 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) - 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: - msg = response['errors']['base'] - raise SRPAuthError(unicode(msg).encode('utf-8')) - - 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/_version.py b/bonafide/src/leap/bonafide/_version.py deleted file mode 100644 index 91fb65cc..00000000 --- a/bonafide/src/leap/bonafide/_version.py +++ /dev/null @@ -1,460 +0,0 @@ - -# 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/src/leap/bonafide/bootstrap.py b/bonafide/src/leap/bonafide/bootstrap.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py deleted file mode 100644 index ae66a0e8..00000000 --- a/bonafide/src/leap/bonafide/config.py +++ /dev/null @@ -1,508 +0,0 @@ -# -*- 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 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 -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 mkdir_p -# check_and_fix_urw_only, get_mtime - - -APPNAME = "bonafide" -ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME - - -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. - - :param domain: the domain to which this providerconfig belongs to. - :type domain: str - :returns: the path - :rtype: str - """ - # 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): - """ - 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 Provider(object): - # 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=None, - check_certificate=True): - 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 - - 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_json() - - if not is_configured and autoconf: - log.msg('provider %s not configured: downloading files...' % - domain) - self.bootstrap() - else: - log.msg('Provider already initialized') - self.first_bootstrap[self._domain] = defer.succeed( - 'already_initialized') - 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 - if not is_file(provider_json): - 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): - 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) - self.ongoing_bootstrap[domain] = d - - def callWhenMainConfigReady(self, cb, *args, **kw): - d = self.first_bootstrap[self._domain] - d.addCallback(lambda _: cb(*args, **kw)) - return d - - def callWhenReady(self, cb, *args, **kw): - d = self.ongoing_bootstrap[self._domain] - d.addCallback(lambda _: cb(*args, **kw)) - return d - - def has_valid_certificate(self): - pass - - 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. - """ - # 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) 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_json()) - d.addErrback(log.err) - return d - - def update_provider_info(self): - """ - Get more recent copy of provider.json from the api URL. - """ - pass - - 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): - # TODO 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 - - # Services config files - - def has_fetched_services_config(self): - return os.path.isfile(self._get_configs_path()) - - def maybe_download_services_config(self, ignored): - - # TODO --- currently, some providers (mail.bitmask.net) raise 401 - # UNAUTHENTICATED if we try to get the services - # See: # https://leap.se/code/issues/7906 - - 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 - - uri, met, path = self._get_configs_download_params() - - 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.netloc - - 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 - - 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): - 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 - all_services]) - return has_all - - def _get_provider_json_path(self): - domain = self._domain.encode(sys.getfilesystemencoding()) - 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_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)) - - api_uri = self._provider_config.api_uri - if api_uri: - parsed = urlparse(api_uri) - self._disco.netloc = 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: - 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 _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 - - def _http_request(self, *args, **kw): - # XXX pass if-modified-since header - return httpRequest(self._agent, *args, **kw) - - -class Record(object): - def __init__(self, **kw): - self.__dict__.update(kw) - - -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) - - -if __name__ == '__main__': - - def print_done(): - print '>>> bootstrapping done!!!' - - provider = Provider('cdev.bitmask.net') - provider.callWhenReady(print_done) - reactor.run() diff --git a/bonafide/src/leap/bonafide/cred_srp.py b/bonafide/src/leap/bonafide/cred_srp.py deleted file mode 100644 index 9fcb97bc..00000000 --- a/bonafide/src/leap/bonafide/cred_srp.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- 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 Session - - -@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.is_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 = Session(credentials) - d = 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 deleted file mode 100644 index b44dae2e..00000000 --- a/bonafide/src/leap/bonafide/provider.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- 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. -""" - -from copy import deepcopy -import re -from urlparse import urlparse - - -""" -Maximum API version number supported by bonafide -""" -MAX_API_VERSION = 1 - - -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) - - Beware that currently they cannot be inherited from bases. - """ - - 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) - - 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 - - newdct = _generate_action_funs(dct) - return super(_MetaActionDispatcher, meta).__new__( - meta, name, bases, newdct) - - -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 - if version > MAX_API_VERSION: - self.version = MAX_API_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 - 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 - # TODO missing UPDATE USER RECORD - - __metaclass__ = _MetaActionDispatcher - _actions = { - 'signup': ('users', 'POST'), - 'update_user': ('users/{uid}', 'PUT'), - 'handshake': ('sessions', 'POST'), - 'authenticate': ('sessions/{login}', 'PUT'), - 'logout': ('logout', 'DELETE'), - 'vpn_cert': ('cert', 'POST'), - 'smtp_cert': ('smtp_cert', 'POST'), - } - - # 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 - - 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'), - } - - def _get_base_url(self): - return "https://{0}".format(self.netloc) - - def get_base_uri(self): - return self._get_base_url() - - # 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 - - def _get_method(self, action_name): - _, method = self._actions.get(action_name) - return method - - -def validate_username(username): - accepted_characters = '^[a-z0-9\-\_\.]*$' - if not re.match(accepted_characters, username): - raise ValueError('Only lowercase letters, digits, . - and _ allowed.') diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py deleted file mode 100644 index 14585eff..00000000 --- a/bonafide/src/leap/bonafide/service.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- 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. -""" -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 - -from twisted.internet import defer -from twisted.python import log - - -_preffix = get_path_prefix() - - -class BonafideService(HookableService): - - 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 - # to the latest authenticated user. - self._active_user = None - - def startService(self): - log.msg('Starting Bonafide Service') - super(BonafideService, self).startService() - - # Commands - - def do_authenticate(self, username, password): - - def notify_passphrase_entry(username, password): - data = dict(username=username, password=password) - self.trigger_hook('on_passphrase_entry', **data) - - def notify_bonafide_auth(result): - if not result: - msg = "Authentication hook did not return anything" - log.msg(msg) - raise RuntimeError(msg) - - token, uuid = result - data = dict(username=username, token=token, uuid=uuid, - password=password) - self.trigger_hook('on_bonafide_auth', **data) - - self._active_user = username - 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) - 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: {'signup': 'ok', 'user': response}) - return d - - def do_logout(self, username): - if not username: - username = self._active_user - - def reset_active(passthrough): - self._active_user = None - return passthrough - - d = self._bonafide.do_logout(username) - d.addCallback(reset_active) - 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 - - def do_get_active_user(self): - user = self._active_user or '' - info = {'user': user} - return defer.succeed(info) diff --git a/bonafide/src/leap/bonafide/services/TODO b/bonafide/src/leap/bonafide/services/TODO deleted file mode 100644 index 97dc639a..00000000 --- a/bonafide/src/leap/bonafide/services/TODO +++ /dev/null @@ -1 +0,0 @@ -# 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 deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/src/leap/bonafide/services/eip.py b/bonafide/src/leap/bonafide/services/eip.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/src/leap/bonafide/services/mail.py b/bonafide/src/leap/bonafide/services/mail.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/src/leap/bonafide/services/soledad.py b/bonafide/src/leap/bonafide/services/soledad.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py deleted file mode 100644 index 4180041a..00000000 --- a/bonafide/src/leap/bonafide/session.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- 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. -""" -from twisted.internet import defer, reactor -from twisted.python import log - -from leap.bonafide import _srp -from leap.bonafide import provider -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): - # 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 - # TODO merge self._request with config.Provider._http_request ? - - self.username = credentials.username - self.password = credentials.password - self._provider_cert = provider_cert - self._api = api - self._initialize_session() - - def _initialize_session(self): - self._agent = cookieAgentFactory(self._provider_cert) - username = self.username or '' - password = self.password or '' - self._srp_auth = _srp.SRPAuthMechanism(username, password) - self._srp_signup = _srp.SRPSignupMechanism() - self._token = None - self._uuid = None - - # Session - - @property - def token(self): - return self._token - - @property - def uuid(self): - return self._uuid - - @property - def is_authenticated(self): - return self._srp_auth.srp_user.authenticated() - - @defer.inlineCallbacks - def authenticate(self): - 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() - - handshake = yield self._request(self._agent, uri, values=params, - method=met) - - 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() - - auth = yield self._request(self._agent, uri, values=params, - method=met) - - uuid, token = self._srp_auth.process_authentication(auth) - self._srp_auth.verify_authentication() - - self._uuid = uuid - self._token = token - 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', auth - print 'resetting user/pass' - self.username = None - self.password = None - self._initialize_session() - defer.returnValue(OK) - - # 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 = 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): - # FIXME to be implemented - 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 - import sys - from twisted.cred.credentials import UsernamePassword - - 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 - return result - - def cbShutDown(ignored): - reactor.stop() - - 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(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) - reactor.run() diff --git a/bonafide/src/leap/bonafide/ssh_service.py b/bonafide/src/leap/bonafide/ssh_service.py deleted file mode 100644 index 3777b10f..00000000 --- a/bonafide/src/leap/bonafide/ssh_service.py +++ /dev/null @@ -1,2 +0,0 @@ -# TODO expose bonafide protocol through ssh, accessible through a hidden -# service diff --git a/bonafide/src/leap/bonafide/tests/__init__.py b/bonafide/src/leap/bonafide/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bonafide/versioneer.py b/bonafide/versioneer.py deleted file mode 100644 index c010f63e..00000000 --- a/bonafide/versioneer.py +++ /dev/null @@ -1,1699 +0,0 @@ - -# 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) diff --git a/src/leap/bitmask/bonafide/__init__.py b/src/leap/bitmask/bonafide/__init__.py new file mode 100644 index 00000000..74f4e668 --- /dev/null +++ b/src/leap/bitmask/bonafide/__init__.py @@ -0,0 +1,4 @@ + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions diff --git a/src/leap/bitmask/bonafide/_http.py b/src/leap/bitmask/bonafide/_http.py new file mode 100644 index 00000000..8f05b421 --- /dev/null +++ b/src/leap/bitmask/bonafide/_http.py @@ -0,0 +1,95 @@ +# -*- 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 base64 +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', 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): + # print "RESPONSE CODE", response.code + 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/src/leap/bitmask/bonafide/_protocol.py b/src/leap/bitmask/bonafide/_protocol.py new file mode 100644 index 00000000..726185ea --- /dev/null +++ b/src/leap/bitmask/bonafide/_protocol.py @@ -0,0 +1,174 @@ +# -*- 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.provider import Api +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 +from twisted.python import log + + +# TODO [ ] enable-disable services +# TODO [ ] read provider info + +COMMANDS = 'signup', 'authenticate', 'logout', 'stats' +_preffix = get_path_prefix() + + +class BonafideProtocol(object): + """ + Expose the protocol that interacts with the Bonafide Service API. + """ + + _apis = defaultdict(None) + _sessions = defaultdict(None) + + def _get_api(self, provider): + # TODO should get deferred + if provider.domain in self._apis: + return self._apis[provider.domain] + + # TODO defer the autoconfig for the provider if needed... + api = Api(provider.api_uri, provider.version) + self._apis[provider.domain] = api + return api + + def _get_session(self, provider, 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) + provider_pem = _get_provider_ca_path(provider_id) + session = Session(credentials, api, provider_pem) + 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): + 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, provider, full_id, password) + return d + + def _do_signup(self, provider, full_id, password): + + # XXX check it's unauthenticated + def return_user(result, _session): + return_code, user = result + if return_code == OK: + return user + + username, _ = config.get_username_and_provider(full_id) + # XXX get deferred? + 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): + _, provider_id = config.get_username_and_provider(full_id) + + provider = config.Provider(provider_id) + + def maybe_finish_provider_bootstrap(result, provider): + session = self._get_session(provider, full_id, password) + d = provider.download_services_config_with_auth(session) + d.addCallback(lambda _: result) + return d + + d = provider.callWhenReady( + self._do_authenticate, provider, full_id, password) + d.addCallback(maybe_finish_provider_bootstrap, provider) + return d + + def _do_authenticate(self, provider, 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) + + # XXX get deferred? + 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): + # XXX use the AVATAR here + log.msg('LOGOUT for %s' % full_id) + 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)) + d.addCallback(lambda _: '%s logged out' % full_id) + return d + + def do_get_smtp_cert(self, full_id): + 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): + # 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 {'sessions': len(self._sessions), + 'mem': '%s KB' % (mem / 1024)} + + +def _get_provider_ca_path(provider_id): + return os.path.join( + _preffix, 'leap', 'providers', provider_id, 'keys', 'ca', 'cacert.pem') diff --git a/src/leap/bitmask/bonafide/_srp.py b/src/leap/bitmask/bonafide/_srp.py new file mode 100644 index 00000000..38f657b3 --- /dev/null +++ b/src/leap/bitmask/bonafide/_srp.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# _srp.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 __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) + self.M = self.srp_user.process_challenge(unhex_salt, unhex_B) + + def get_authentication_params(self): + # It looks A is not used server side + 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) + 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: + msg = response['errors']['base'] + raise SRPAuthError(unicode(msg).encode('utf-8')) + + 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/src/leap/bitmask/bonafide/_version.py b/src/leap/bitmask/bonafide/_version.py new file mode 100644 index 00000000..91fb65cc --- /dev/null +++ b/src/leap/bitmask/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/src/leap/bitmask/bonafide/bootstrap.py b/src/leap/bitmask/bonafide/bootstrap.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/bonafide/config.py b/src/leap/bitmask/bonafide/config.py new file mode 100644 index 00000000..ae66a0e8 --- /dev/null +++ b/src/leap/bitmask/bonafide/config.py @@ -0,0 +1,508 @@ +# -*- 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 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 +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 mkdir_p +# check_and_fix_urw_only, get_mtime + + +APPNAME = "bonafide" +ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME + + +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. + + :param domain: the domain to which this providerconfig belongs to. + :type domain: str + :returns: the path + :rtype: str + """ + # 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): + """ + 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 Provider(object): + # 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=None, + check_certificate=True): + 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 + + 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_json() + + if not is_configured and autoconf: + log.msg('provider %s not configured: downloading files...' % + domain) + self.bootstrap() + else: + log.msg('Provider already initialized') + self.first_bootstrap[self._domain] = defer.succeed( + 'already_initialized') + 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 + if not is_file(provider_json): + 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): + 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) + self.ongoing_bootstrap[domain] = d + + def callWhenMainConfigReady(self, cb, *args, **kw): + d = self.first_bootstrap[self._domain] + d.addCallback(lambda _: cb(*args, **kw)) + return d + + def callWhenReady(self, cb, *args, **kw): + d = self.ongoing_bootstrap[self._domain] + d.addCallback(lambda _: cb(*args, **kw)) + return d + + def has_valid_certificate(self): + pass + + 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. + """ + # 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) 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_json()) + d.addErrback(log.err) + return d + + def update_provider_info(self): + """ + Get more recent copy of provider.json from the api URL. + """ + pass + + 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): + # TODO 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 + + # Services config files + + def has_fetched_services_config(self): + return os.path.isfile(self._get_configs_path()) + + def maybe_download_services_config(self, ignored): + + # TODO --- currently, some providers (mail.bitmask.net) raise 401 + # UNAUTHENTICATED if we try to get the services + # See: # https://leap.se/code/issues/7906 + + 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 + + uri, met, path = self._get_configs_download_params() + + 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.netloc + + 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 + + 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): + 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 + all_services]) + return has_all + + def _get_provider_json_path(self): + domain = self._domain.encode(sys.getfilesystemencoding()) + 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_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)) + + api_uri = self._provider_config.api_uri + if api_uri: + parsed = urlparse(api_uri) + self._disco.netloc = 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: + 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 _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 + + def _http_request(self, *args, **kw): + # XXX pass if-modified-since header + return httpRequest(self._agent, *args, **kw) + + +class Record(object): + def __init__(self, **kw): + self.__dict__.update(kw) + + +class WebClientContextFactory(ClientContextFactory): + def getContext(self, hostname, port): + return ClientContextFactory.getContext(self) + + +if __name__ == '__main__': + + def print_done(): + print '>>> bootstrapping done!!!' + + provider = Provider('cdev.bitmask.net') + provider.callWhenReady(print_done) + reactor.run() diff --git a/src/leap/bitmask/bonafide/cred_srp.py b/src/leap/bitmask/bonafide/cred_srp.py new file mode 100644 index 00000000..9fcb97bc --- /dev/null +++ b/src/leap/bitmask/bonafide/cred_srp.py @@ -0,0 +1,157 @@ +# -*- 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 Session + + +@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.is_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 = Session(credentials) + d = 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/src/leap/bitmask/bonafide/provider.py b/src/leap/bitmask/bonafide/provider.py new file mode 100644 index 00000000..b44dae2e --- /dev/null +++ b/src/leap/bitmask/bonafide/provider.py @@ -0,0 +1,186 @@ +# -*- 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. +""" + +from copy import deepcopy +import re +from urlparse import urlparse + + +""" +Maximum API version number supported by bonafide +""" +MAX_API_VERSION = 1 + + +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) + + Beware that currently they cannot be inherited from bases. + """ + + 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) + + 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 + + newdct = _generate_action_funs(dct) + return super(_MetaActionDispatcher, meta).__new__( + meta, name, bases, newdct) + + +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 + if version > MAX_API_VERSION: + self.version = MAX_API_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 + 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 + # TODO missing UPDATE USER RECORD + + __metaclass__ = _MetaActionDispatcher + _actions = { + 'signup': ('users', 'POST'), + 'update_user': ('users/{uid}', 'PUT'), + 'handshake': ('sessions', 'POST'), + 'authenticate': ('sessions/{login}', 'PUT'), + 'logout': ('logout', 'DELETE'), + 'vpn_cert': ('cert', 'POST'), + 'smtp_cert': ('smtp_cert', 'POST'), + } + + # 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 + + 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'), + } + + def _get_base_url(self): + return "https://{0}".format(self.netloc) + + def get_base_uri(self): + return self._get_base_url() + + # 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 + + def _get_method(self, action_name): + _, method = self._actions.get(action_name) + return method + + +def validate_username(username): + accepted_characters = '^[a-z0-9\-\_\.]*$' + if not re.match(accepted_characters, username): + raise ValueError('Only lowercase letters, digits, . - and _ allowed.') diff --git a/src/leap/bitmask/bonafide/service.py b/src/leap/bitmask/bonafide/service.py new file mode 100644 index 00000000..14585eff --- /dev/null +++ b/src/leap/bitmask/bonafide/service.py @@ -0,0 +1,120 @@ +# -*- 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. +""" +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 + +from twisted.internet import defer +from twisted.python import log + + +_preffix = get_path_prefix() + + +class BonafideService(HookableService): + + 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 + # to the latest authenticated user. + self._active_user = None + + def startService(self): + log.msg('Starting Bonafide Service') + super(BonafideService, self).startService() + + # Commands + + def do_authenticate(self, username, password): + + def notify_passphrase_entry(username, password): + data = dict(username=username, password=password) + self.trigger_hook('on_passphrase_entry', **data) + + def notify_bonafide_auth(result): + if not result: + msg = "Authentication hook did not return anything" + log.msg(msg) + raise RuntimeError(msg) + + token, uuid = result + data = dict(username=username, token=token, uuid=uuid, + password=password) + self.trigger_hook('on_bonafide_auth', **data) + + self._active_user = username + 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) + 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: {'signup': 'ok', 'user': response}) + return d + + def do_logout(self, username): + if not username: + username = self._active_user + + def reset_active(passthrough): + self._active_user = None + return passthrough + + d = self._bonafide.do_logout(username) + d.addCallback(reset_active) + 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 + + def do_get_active_user(self): + user = self._active_user or '' + info = {'user': user} + return defer.succeed(info) diff --git a/src/leap/bitmask/bonafide/services/TODO b/src/leap/bitmask/bonafide/services/TODO new file mode 100644 index 00000000..97dc639a --- /dev/null +++ b/src/leap/bitmask/bonafide/services/TODO @@ -0,0 +1 @@ +# XXX this should be discoverable through the config. diff --git a/src/leap/bitmask/bonafide/services/__init__.py b/src/leap/bitmask/bonafide/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/bonafide/services/eip.py b/src/leap/bitmask/bonafide/services/eip.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/bonafide/services/mail.py b/src/leap/bitmask/bonafide/services/mail.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/bonafide/services/soledad.py b/src/leap/bitmask/bonafide/services/soledad.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/bonafide/session.py b/src/leap/bitmask/bonafide/session.py new file mode 100644 index 00000000..4180041a --- /dev/null +++ b/src/leap/bitmask/bonafide/session.py @@ -0,0 +1,219 @@ +# -*- 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. +""" +from twisted.internet import defer, reactor +from twisted.python import log + +from leap.bonafide import _srp +from leap.bonafide import provider +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): + # 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 + # TODO merge self._request with config.Provider._http_request ? + + self.username = credentials.username + self.password = credentials.password + self._provider_cert = provider_cert + self._api = api + self._initialize_session() + + def _initialize_session(self): + self._agent = cookieAgentFactory(self._provider_cert) + username = self.username or '' + password = self.password or '' + self._srp_auth = _srp.SRPAuthMechanism(username, password) + self._srp_signup = _srp.SRPSignupMechanism() + self._token = None + self._uuid = None + + # Session + + @property + def token(self): + return self._token + + @property + def uuid(self): + return self._uuid + + @property + def is_authenticated(self): + return self._srp_auth.srp_user.authenticated() + + @defer.inlineCallbacks + def authenticate(self): + 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() + + handshake = yield self._request(self._agent, uri, values=params, + method=met) + + 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() + + auth = yield self._request(self._agent, uri, values=params, + method=met) + + uuid, token = self._srp_auth.process_authentication(auth) + self._srp_auth.verify_authentication() + + self._uuid = uuid + self._token = token + 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', auth + print 'resetting user/pass' + self.username = None + self.password = None + self._initialize_session() + defer.returnValue(OK) + + # 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 = 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): + # FIXME to be implemented + 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 + import sys + from twisted.cred.credentials import UsernamePassword + + 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 + return result + + def cbShutDown(ignored): + reactor.stop() + + 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(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) + reactor.run() diff --git a/src/leap/bitmask/bonafide/ssh_service.py b/src/leap/bitmask/bonafide/ssh_service.py new file mode 100644 index 00000000..3777b10f --- /dev/null +++ b/src/leap/bitmask/bonafide/ssh_service.py @@ -0,0 +1,2 @@ +# TODO expose bonafide protocol through ssh, accessible through a hidden +# service diff --git a/src/leap/bitmask/bonafide/tests/__init__.py b/src/leap/bitmask/bonafide/tests/__init__.py new file mode 100644 index 00000000..e69de29b -- cgit v1.2.3