pax_global_header00006660000000000000000000000064132444360030014511gustar00rootroot0000000000000052 comment=1ebf1b851446d226983df5f170cb336fc3cf12b7 comitup-master-1.2.3/000077500000000000000000000000001324443600300144655ustar00rootroot00000000000000comitup-master-1.2.3/.gitignore000066400000000000000000000000751324443600300164570ustar00rootroot00000000000000__pycache__ *.pyc dist .cache .coverage *egg-info *swp build comitup-master-1.2.3/.travis.yml000066400000000000000000000005321324443600300165760ustar00rootroot00000000000000language: python sudo: required dist: trusty virtualenv: system_site_packages: true matrix: include: - python: "2.7" addons: apt: packages: - avahi-daemon - python-dbus - python-gobject-2 - python-networkmanager script: - find . - python setup.py build test comitup-master-1.2.3/COPYING000066400000000000000000000432541324443600300155300ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. comitup-master-1.2.3/LICENSE000066400000000000000000000431771324443600300155060ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {description} Copyright (C) {year} {fullname} 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. {signature of Ty Coon}, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. comitup-master-1.2.3/MANIFEST.in000066400000000000000000000001661324443600300162260ustar00rootroot00000000000000include LICENSE include doc/*.ronn include cli/comitupcli.py include conf/comitup* include README.md include web/*.py comitup-master-1.2.3/README.md000066400000000000000000000040171324443600300157460ustar00rootroot00000000000000 Comitup ====== [Home Page](https://davesteele.github.io/comitup/) [Wiki](https://github.com/davesteele/comitup/wiki) Bootstrap Wifi using Wifi ------------------------- The __comitup__ service establishes wifi connectivity for a headless Linux system, using wifi as the only access mechanism to the system. If the computer cannot automatically connect to a local wifi access point, __comitup__ will create a custom hotspot, and establish a __comitup-web__ web service on that network. The web service can be used to remotely select and authenticate a visible wifi connection. The hotspot is named _comitup-<nnnn>_, where _<nnnn>_ is a persistent 4-digit number. The website is accessible on that hotspot as _ht​tp://comitup.local_ or _ht​tp://comitup-<nnnn>.local_ from any device which supports [Bonjour/ZeroConf/Avahi] [zeroconf]. For other devices, use a Zeroconf browser ([Android][], [Windows][]) to determine the IP address of the "Comitup Service", and browse to _http://<ipaddress>_. In most cases, this address will be _http://10.42.0.1/_ If two wifi interfaces are available, the first will persistently remain the hotspot, and the second will get the external connection. When both are connected, forwarding and masquerading are enabled so that hotspot-connected devices can access external networks. [zeroconf]: https://en.wikipedia.org/wiki/Zero-configuration_networking [Android]: https://play.google.com/store/apps/details?id=com.melloware.zeroconf&hl=en [Windows]: http://hobbyistsoftware.com/bonjourbrowser The __comitup-cli__ utility is available to interact with _comitup_ from a local terminal session. __comitup__ requires NetworkManager and systemd. Man pages --------- * [comitup.8](https://davesteele.github.io/comitup/man/comitup.8.html) * [comitup-conf.5](https://davesteele.github.io/comitup/man/comitup-conf.5.html) * [comitup-web.8](https://davesteele.github.io/comitup/man/comitup-web.8.html) * [comitup-cli.1](https://davesteele.github.io/comitup/man/comitup-cli.1.html) comitup-master-1.2.3/cli/000077500000000000000000000000001324443600300152345ustar00rootroot00000000000000comitup-master-1.2.3/cli/__init__.py000066400000000000000000000000001324443600300173330ustar00rootroot00000000000000comitup-master-1.2.3/cli/comitupcli.py000077500000000000000000000057351324443600300177730ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import sys sys.path.append('.') sys.path.append('..') from collections import namedtuple, OrderedDict # noqa from getpass import getpass # noqa from comitup import client as ciu # noqa def do_reload(connection): pass def do_quit(connection): sys.exit(0) def do_delete(connection): ciu.ciu_delete(connection) def do_connect(ssid, password): ciu.ciu_connect(ssid, password) def do_info(connection): info = ciu.ciu_info(connection) print("") print("Host %s on comitup version %s" % (info['hostnames'], info['version'])) CmdState = namedtuple('CmdState', "fn, desc, HOTSPOT, CONNECTING, CONNECTED") commands = OrderedDict([ ('i', CmdState(do_info, '(i)nfo', True, True, True)), ('r', CmdState(do_reload, '(r)eload', True, True, True)), ('d', CmdState(do_delete, '(d)elete connection', False, True, True)), ('q', CmdState(do_quit, '(q)uit', True, True, True)), ('', CmdState(do_connect, 'connect to ', True, False, False)), ('m', CmdState(do_connect, '(m)anual connection', True, False, False)), ]) def int_value(s): try: return int(s) except (ValueError, TypeError): return None def get_state(): state, connection = ciu.ciu_state() return state, connection def get_valid_cmds(state): cmds = [x for x in commands.keys() if commands[x].__getattribute__(state)] return cmds def print_cmd_prompts(state, connection, points): print('') print("State: %s" % state) print("Connection: %s" % connection) if state == 'HOTSPOT': print("Points:") for point in enumerate(points, start=1): print(" %d: %s" % (point[0], point[1]['ssid'])) print("Available commands:") for cmd in get_valid_cmds(state): print(" %s" % commands[cmd].desc) def interpreter(): while True: state, connection = get_state() points = ciu.ciu_points() print_cmd_prompts(state, connection, points) cmd = input("command?: ") index = int_value(cmd) if index: password = "" if points[index-1]['security'] == 'encrypted': password = getpass('password: ') do_connect(points[index-1]['ssid'], password) elif cmd == 'm': ssid = input("ssid?: ") password = getpass('password (if required)?: ') do_connect(ssid, password) else: try: commands[cmd].fn(connection) except KeyError: print("\nInvalid command\n") if __name__ == '__main__': interpreter() comitup-master-1.2.3/comitup/000077500000000000000000000000001324443600300161455ustar00rootroot00000000000000comitup-master-1.2.3/comitup/__init__.py000066400000000000000000000000001324443600300202440ustar00rootroot00000000000000comitup-master-1.2.3/comitup/client.py000077500000000000000000000026551324443600300200100ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import dbus import sys from collections import defaultdict func_map = defaultdict(lambda: None) def ciu_decorator(fn): def wrapper(*args, **kwargs): endpoint = fn() if func_map[endpoint] is None: try: bus = dbus.SystemBus() ciu_service = bus.get_object( 'com.github.davesteele.comitup', '/com/github/davesteele/comitup' ) func_map[endpoint] = ciu_service.get_dbus_method( endpoint, 'com.github.davesteele.comitup' ) except dbus.exceptions.DBusException: print("Error connecting to the comitup D-Bus service") sys.exit(1) return func_map[endpoint](*args, **kwargs) return wrapper @ciu_decorator def ciu_state(): return 'state' @ciu_decorator def ciu_points(): return 'access_points' @ciu_decorator def ciu_delete(): return 'delete_connection' @ciu_decorator def ciu_connect(): return 'connect' @ciu_decorator def ciu_ifo(): return 'get_info' comitup-master-1.2.3/comitup/comitup.py000077500000000000000000000050361324443600300202060ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import os import logging from logging.handlers import TimedRotatingFileHandler from comitup import persist from comitup import config import random import argparse import sys from gi.repository.GLib import MainLoop from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) from comitup import statemgr # noqa from comitup import webmgr # noqa from comitup import iptmgr # noqa from comitup import wificheck # noqa PERSIST_PATH = "/var/lib/comitup/comitup.json" CONF_PATH = "/etc/comitup.conf" LOG_PATH = "/var/log/comitup.log" def deflog(): log = logging.getLogger('comitup') log.setLevel(logging.INFO) handler = TimedRotatingFileHandler( LOG_PATH, encoding='utf=8', when='D', interval=7, backupCount=8, ) fmtr = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) handler.setFormatter(fmtr) log.addHandler(handler) return log def load_data(): conf = config.Config( CONF_PATH, defaults={ 'base_name': 'comitup', 'web_service': '', 'external_callback': '/usr/local/bin/comitup-callback', }, ) data = persist.persist( PERSIST_PATH, {'id': str(random.randrange(1000, 9999))}, ) return (conf, data) def inst_name(conf, data): return conf.base_name + '-' + data.id def parse_args(): parser = argparse.ArgumentParser(description="") parser.add_argument('-c', '--check', action='store_true', help="Check the wifi devices and exit") args = parser.parse_args() return args def main(): if os.geteuid() != 0: exit("Comitup requires root privileges") args = parse_args() log = deflog() log.info("Starting comitup") (conf, data) = load_data() if args.check: if wificheck.run_checks(): sys.exit(1) else: sys.exit(0) else: wificheck.run_checks(verbose=False) webmgr.init_webmgr(conf.web_service) iptmgr.init_iptmgr() statemgr.init_state_mgr( conf, data, [webmgr.state_callback, iptmgr.state_callback], ) loop = MainLoop() loop.run() if __name__ == '__main__': main() comitup-master-1.2.3/comitup/config.py000066400000000000000000000015161324443600300177670ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import configparser import io class Config(object): def __init__(self, filename, section='DEFAULT', defaults={}): self._section = section self._config = configparser.SafeConfigParser(defaults=defaults) conf_str = '[%s]\n' % self._section + open(filename, 'r').read() conf_fp = io.StringIO(conf_str) self._config.readfp(conf_fp) def __getattr__(self, tag): try: return self._config.get(self._section, tag) except configparser.NoOptionError: raise AttributeError comitup-master-1.2.3/comitup/iptmgr.py000066400000000000000000000055721324443600300200320ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import logging import subprocess from comitup import modemgr from comitup import nm start_cmds = [ # HOTSPOT rules "iptables -w -N COMITUP-OUT", "iptables -w -A COMITUP-OUT " "-p icmp --icmp-type destination-unreachable -j DROP", # noqa "iptables -w -A COMITUP-OUT " "-p icmp --icmp-type port-unreachable -j DROP", # noqa "iptables -w -A COMITUP-OUT -j RETURN", "iptables -w -I OUTPUT -j COMITUP-OUT", ] end_cmds = [ # Clear HOTSPOT rules "iptables -w -D OUTPUT -j COMITUP-OUT >/dev/null 2>&1", "iptables -w -D COMITUP-OUT " "-p icmp --icmp-type destination-unreachable " # noqa "-j DROP >/dev/null 2>&1", # noqa "iptables -w -D COMITUP-OUT " "-p icmp --icmp-type port-unreachable " # noqa "-j DROP >/dev/null 2>&1", # noqa "iptables -w -D COMITUP-OUT -j RETURN >/dev/null 2>&1", "iptables -w -X COMITUP-OUT >/dev/null 2>&1", ] appliance_cmds = [ "iptables -w -t nat -N COMITUP-FWD", "iptables -w -t nat -A COMITUP-FWD -o {} -j MASQUERADE", "iptables -w -t nat -A COMITUP-FWD -j RETURN", "iptables -w -t nat -A POSTROUTING -j COMITUP-FWD", "echo 1 > /proc/sys/net/ipv4/ip_forward", ] appliance_clear = [ "iptables -w -t nat -D POSTROUTING -j COMITUP-FWD >/dev/null 2>&1", "iptables -w -t nat -D COMITUP-FWD -o {} -j MASQUERADE >/dev/null 2>&1", "iptables -w -t nat -D COMITUP-FWD -j RETURN >/dev/null 2>&1", "iptables -w -t nat -X COMITUP-FWD >/dev/null 2>&1", ] log = logging.getLogger('comitup') def run_cmds(cmds): outdev = nm.device_name(modemgr.get_link_device()) for cmd in cmds: subprocess.call(cmd.format(outdev), shell=True) def state_callback(state, action): if (state, action) == ('HOTSPOT', 'start'): log.debug("Running iptables commands for HOTSPOT") run_cmds(end_cmds) run_cmds(start_cmds) if modemgr.get_mode() == 'router': run_cmds(appliance_clear) log.debug("Done with iptables commands for HOTSPOT") elif (state, action) == ('CONNECTED', 'start'): log.debug("Running iptables commands for CONNECTING") run_cmds(end_cmds) if modemgr.get_mode() == 'router': run_cmds(appliance_clear) run_cmds(appliance_cmds) log.debug("Done with iptables commands for CONNECTING") def init_iptmgr(): pass def main(): import six print("applying rules") run_cmds(start_cmds) six.input("Press Enter to continue...") run_cmds(end_cmds) if __name__ == '__main__': main() comitup-master-1.2.3/comitup/iwscan.py000066400000000000000000000054411324443600300200070ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import subprocess import re from multiprocessing import Process, Queue # NetworkManager is doing a poor job of maintaining the AP scan list when # in AP mode. Use the 'iw' command to get the AP list. def docmd(cmd): cmd = "timeout 5 " + cmd try: out = subprocess.check_output(cmd.split()).decode() except subprocess.CalledProcessError: out = "" return out def devlist(): """Get a list of supported devices from 'iw'""" out = docmd("iw dev") devs = [x.split()[1] for x in out.split('\n') if "Interface" in x] return devs def blk2dict(blk): """Convert a 'iw dev scan' block into a tagged dict""" lines = [x.strip().split(':') for x in blk.split('\n') if ':' in x] tups = [(x[0].strip(), x[1].strip()) for x in lines if len(x) > 1] return dict(tups) def dbm2pct(dbm): pct = (dbm + 100.0) * 2 pct = max(0, pct) pct = min(100, pct) return str(pct) def devaps(dev): """Get a list of Access Points (as dicts) for a device""" out = docmd('iw dev %s scan' % dev) aps = [] for blk in re.split('\nBSS ', out[4:]): try: ap = blk2dict(blk) ap['power'] = dbm2pct(float(ap['signal'].split()[0])) if ap['SSID']: aps.append(ap) except KeyError: pass return aps def apgen(dev, q): for ap in devaps(dev): pt = {} pt['ssid'] = ap['SSID'] pt['strength'] = ap['power'] if 'WPA' in ap or 'RSN' in ap: pt['security'] = 'encrypted' else: pt['security'] = 'unencrypted' q.put(pt) q.put("DONE") def dedup_aplist(aplist): apdict = {x['ssid']: x for x in aplist} return [apdict[x] for x in apdict] def candidates(device=None): """Return a list of reachable Access Point SSIDs, sorted by power""" if device: dev_list = [device] else: dev_list = devlist() jobs = [] q = Queue() for dev in dev_list: p = Process(target=apgen, args=(dev, q)) p.start() jobs.append(p) clist = [] donecount = 0 while donecount < len(jobs): pt = q.get() if pt == "DONE": donecount += 1 else: clist.append(pt) for p in jobs: p.join() clist = dedup_aplist(clist) clist = sorted(clist, key=lambda x: -float(x['strength'])) return clist def ap_conn_count(): count = 0 for dev in devlist(): out = docmd('iw dev %s station dump' % dev) count += len([x for x in out.split('\n') if "Station" in x]) return count if __name__ == '__main__': print(candidates()) print(ap_conn_count()) comitup-master-1.2.3/comitup/mdns.py000077500000000000000000000061701324443600300174670ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import dbus import socket import logging from comitup import nm import subprocess log = logging.getLogger('comitup') # globals CLASS_IN = 0x01 TYPE_A = 0x01 TTL = 5 DBUS_NAME = 'org.freedesktop.Avahi' DBUS_PATH_SERVER = '/' DBUS_INTERFACE_SERVER = 'org.freedesktop.Avahi.Server' DBUS_INTERFACE_ENTRY_GROUP = 'org.freedesktop.Avahi.EntryGroup' PROTO_INET = 0 group = None # functions def establish_group(): global group bus = dbus.SystemBus() server = dbus.Interface( bus.get_object(DBUS_NAME, DBUS_PATH_SERVER), DBUS_INTERFACE_SERVER ) group = dbus.Interface( bus.get_object(DBUS_NAME, server.EntryGroupNew()), DBUS_INTERFACE_ENTRY_GROUP ) def encode_dns(name): components = [x for x in name.split('.') if len(x) > 0] fixed_name = '.'.join(components) return fixed_name.encode('ascii') def make_a_record(host, devindex, addr): group.AddRecord( devindex, PROTO_INET, dbus.UInt32(0), encode_dns(host), CLASS_IN, TYPE_A, TTL, socket.inet_aton(addr) ) def string_to_txt_array(strng): if strng: return [dbus.Byte(x) for x in strng.encode()] else: return strng def string_array_to_txt_array(txt_array): return [string_to_txt_array(x) for x in txt_array] def add_service(host, devindex, addr): name = host if '.local' in name: name = name[:-len('.local')] group.AddService( devindex, PROTO_INET, dbus.UInt32(0), name, "_workstation._tcp", "", host, dbus.UInt16(9), string_array_to_txt_array([ "hostname=%s" % host.encode(), "ipaddr=%s" % addr, "comitup-home=https://davesteele.github.io/comitup/", ]) ) # public functions def clear_entries(): if group and not group.IsEmpty(): group.Reset() establish_group() def get_interface_mapping(): mapping = {} for line in subprocess.check_output("ip addr".split()).decode().split('\n'): try: asc_index, name = line.split(": ")[0:2] mapping[name] = int(asc_index) except ValueError: pass return mapping def add_hosts(hosts): establish_group() int_mapping = get_interface_mapping() for device in nm.get_devices(): name = nm.device_name(device) addr = nm.get_active_ip(device) if (name in nm.get_phys_dev_names() \ and name in int_mapping \ and addr): index = int_mapping[name] for host in hosts: make_a_record(host, index, addr) add_service(hosts[0], index, addr) group.Commit() if __name__ == '__main__': add_hosts(['comitup-1111.local', 'comitup.local']) while True: pass comitup-master-1.2.3/comitup/modemgr.py000066400000000000000000000021121324443600300201450ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # from comitup import nm from comitup import config SINGLE_MODE = "single" MULTI_MODE = "router" CONF_PATH = "/etc/comitup.conf" def dual_enabled(): conf = config.Config(CONF_PATH, defaults={'enable_appliance_mode': 'true'}) return conf.enable_appliance_mode == 'true' def get_mode(): if len(nm.get_wifi_devices()) > 1 and dual_enabled(): return MULTI_MODE else: return SINGLE_MODE def get_ap_device(): return nm.get_wifi_device(0) def get_link_device(): second_device = nm.get_wifi_device(1) if second_device and dual_enabled(): return second_device else: return nm.get_wifi_device(0) def get_state_device(state): if state == 'HOTSPOT': return get_ap_device() else: return get_link_device() comitup-master-1.2.3/comitup/nm.py000077500000000000000000000226261324443600300171440ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import logging import NetworkManager as nm import argparse import dbus import sys import uuid import getpass from functools import wraps from comitup import iwscan import pprint pp = pprint.PrettyPrinter(indent=4) log = logging.getLogger('comitup') def initialize(): nm.Settings.ReloadConnections() def nm_state(): return nm.NetworkManager.State def none_on_exception(*exceptions): def _none_on_exception(fp): @wraps(fp) def wrapper(*args, **kwargs): try: return fp(*args, **kwargs) except exceptions: log.debug("Got an exception, returning None, %s" % fp.__name__) return None return wrapper return _none_on_exception def get_devices(): try: return nm.NetworkManager.GetDevices() except TypeError: # NetworkManager is gone for some reason. Bail big time. sys.exit(1) def device_name(device): return device.Interface def get_wifi_devices(): return [x for x in get_devices() if x.DeviceType == 2] def get_phys_dev_names(): return [device_name(x) for x in get_devices() if x.DeviceType in (1, 2)] @none_on_exception(IndexError) def get_wifi_device(index=0): return get_wifi_devices()[index] def get_device_path(device): return device.SpecificDevice().object_path def disconnect(device): try: device.Disconnect() except: log.debug("Error received in disconnect") def get_device_settings(device): connection = device.ActiveConnection return connection.Connection.GetSettings() @none_on_exception(AttributeError) def get_active_ssid(device): return get_device_settings(device)['802-11-wireless']['ssid'] @none_on_exception(AttributeError, IndexError) def get_active_ip(device): return device.Ip4Config.Addresses[0][0] def get_all_connections(): return [x for x in nm.Settings.ListConnections()] @none_on_exception(AttributeError, KeyError) def get_ssid_from_connection(connection): settings = connection.GetSettings() return settings['802-11-wireless']['ssid'] def get_connection_by_ssid(name): for connection in get_all_connections(): ssid = get_ssid_from_connection(connection) if name == ssid: return connection return None def del_connection_by_ssid(name): for connection in get_all_connections(): ssid = get_ssid_from_connection(connection) if name == ssid: connection.Delete() def activate_connection_by_ssid(ssid, device, path='/'): connection = get_connection_by_ssid(ssid) nm.NetworkManager.ActivateConnection(connection, device, path) def deactivate_connection(device): connection = device.ActiveConnection if connection: nm.NetworkManager.DeactivateConnection(connection) @none_on_exception(AttributeError) def get_access_points(device): return device.SpecificDevice().GetAllAccessPoints() def get_points_ext(device): try: inlist = sorted(get_access_points(device), key=lambda x: -x.Strength) except (TypeError, dbus.exceptions.DBusException): log.debug("Error getting access points") inlist = [] outlist = [] for point in inlist: try: if point.Flags or point.WpaFlags or point.RsnFlags: encstr = "encrypted" else: encstr = "unencrypted" outpoint = { 'ssid': point.Ssid, 'strength': str(point.Strength), 'security': encstr, 'nmpath': point.object_path, } outlist.append(outpoint) except dbus.exceptions.DBusException: log.debug("Error getting point info") return outlist def get_candidate_connections(device): candidates = [] for conn in get_all_connections(): settings = conn.GetSettings() ssid = get_ssid_from_connection(conn) try: if ssid \ and settings['connection']['type'] == '802-11-wireless' \ and settings['802-11-wireless']['mode'] == 'infrastructure': candidates.append(ssid) except KeyError: log.debug("Unexpected connection format for %s" % ssid) points = [x.Ssid for x in get_access_points(device)] iwpoints = [x['ssid'] for x in iwscan.candidates()] return list(set(candidates) & (set(points) | set(iwpoints))) def make_hotspot(name='comitup', device=None): settings = { 'connection': { 'type': '802-11-wireless', 'uuid': str(uuid.uuid4()), 'id': name, 'autoconnect': False, }, '802-11-wireless': { 'mode': 'ap', 'ssid': name, }, 'ipv4': { 'method': 'shared', }, 'ipv6': { 'method': 'ignore', }, } if device: settings['connection']['interface-name'] = device_name(device) nm.Settings.AddConnection(settings) def make_connection_for(ssid, password=None, interface=None): settings = dbus.Dictionary({ 'connection': dbus.Dictionary( { 'id': ssid, 'type': '802-11-wireless', 'uuid': str(uuid.uuid4()), }), '802-11-wireless': dbus.Dictionary( { 'ssid': dbus.ByteArray(ssid.encode()), 'mode': 'infrastructure', }), 'ipv4': dbus.Dictionary( { # assume DHCP 'method': 'auto', }), 'ipv6': dbus.Dictionary( { # assume ipv4-only 'method': 'ignore', }), }) if interface: settings['connection']['interface-name'] = interface # assume privacy = WPA(2) psk if password: settings['802-11-wireless']['security'] = '802-11-wireless-security' settings['802-11-wireless-security'] = dbus.Dictionary({ 'auth-alg': 'open', 'key-mgmt': 'wpa-psk', 'psk': password, }) nm.Settings.AddConnection(settings) # # CLI Interface # def do_listaccess(arg): """List all accessible access points""" rows = [] for point in get_access_points(get_wifi_device()): row = ( point.Ssid, point.HwAddress, point.Flags, point.WpaFlags, point.RsnFlags, point.Strength, point.Frequency ) rows.append(row) bypwr = sorted(rows, key=lambda x: -x[5]) hdrs = ('SSID', 'MAC', 'Private', 'WPA', 'RSN', 'Power', 'Frequency') try: import tabulate print(tabulate.tabulate(bypwr, headers=hdrs)) except: for entry in bypwr: print(entry) def do_listconnections(arg): """List all defined connections""" for connection in get_all_connections(): ssid = get_ssid_from_connection(connection) if ssid: print(ssid) def do_setconnection(ssid): """Connect to a connection""" activate_connection_by_ssid(ssid, get_wifi_device()) def do_getconnection(dummy): """Print the active connection ssid""" print(get_active_ssid(get_wifi_device())) def do_getip(dummy): """Print the current IP address""" print(get_active_ip(get_wifi_device())) def do_detailconnection(ssid): """Print details about a connection""" connection = get_connection_by_ssid(ssid) pp.pprint(connection.GetSettings()) pp.pprint(connection.GetSecrets()) def do_delconnection(ssid): """Delete a connection id'd by ssid""" del_connection_by_ssid(ssid) def do_makehotspot(dummy): """Create a hotspot connection for future use""" make_hotspot() def do_listcandidates(dummy): """List available connections for current access points""" for candidate in get_candidate_connections(get_wifi_device()): print(candidate) def do_makeconnection(ssid): """Create a connection for an access point, for future use""" password = getpass.getpass('password: ') make_connection_for(ssid, password) def get_command(cmd): cmd_name = "do_" + cmd try: return(globals()[cmd_name]) except KeyError: return None def parse_args(): prog = 'comitup' epilog = "Commands:\n" for x in sorted([x for x in globals().keys() if x[0:3] == "do_"]): epilog += " %s - %s\n" % (x[3:], globals()[x].__doc__) parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, prog=prog, description="Manage NetworkManager Wifi connections", epilog=epilog, ) parser.add_argument( 'command', help="command", ) parser.add_argument( 'arg', nargs='?', help="command argument", ) args = parser.parse_args() if get_command(args.command) is None: parser.error("Invalid command") return args def main(): try: initialize() except dbus.exceptions.DBusException: print("Must run as root") sys.exit(1) args = parse_args() fn = get_command(args.command) fn(args.arg) if __name__ == '__main__': main() comitup-master-1.2.3/comitup/nmmon.py000077500000000000000000000061051324443600300176500ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import dbus from gi.repository.GLib import MainLoop from dbus.mainloop.glib import DBusGMainLoop import logging if __name__ == '__main__': DBusGMainLoop(set_as_default=True) from comitup import nm # noqa from comitup import modemgr # noqa # # globals # log = logging.getLogger('comitup') bus = dbus.SystemBus() device_path = None device_listener = None comstate = 'HOTSPOT' def null_fn(): pass nm_dev_connect = null_fn nm_dev_fail = null_fn # # functions # def set_device_callbacks(state, up, down): global nm_dev_connect global nm_dev_fail global comstate nm_dev_connect = null_fn nm_dev_fail = null_fn comstate = state check_device_listener() nm_dev_connect = up nm_dev_fail = down def nm_device_change(state, *args): # see for device states: # https://developer.gnome.org/NetworkManager/stable/spec.html # #type-NM_DEVICE_STATE if state == 100: nm_dev_connect() elif state == 120: nm_dev_fail() def set_device_listener(path): global device_listener if device_listener: device_listener.remove() log.debug("adding listener for path %s" % path) device_listener = bus.add_signal_receiver( nm_device_change, signal_name="StateChanged", dbus_interface="org.freedesktop.NetworkManager.Device", path=path ) def check_device_listener(force=False): global device_path current_path = nm.get_device_path(modemgr.get_state_device(comstate)) if force or (current_path and current_path != device_path): device_path = current_path set_device_listener(device_path) def nm_state_change(state): global device_path # https://developer.gnome.org/NetworkManager/stable/spec.html # #type-NM_STATE if state >= 50: check_device_listener() def set_nm_listeners(): bus.add_signal_receiver( check_device_listener, signal_name="DeviceAdded", dbus_interface="org.freedesktop.NetworkManager" ) bus.add_signal_receiver( check_device_listener, signal_name="DeviceRemoved", dbus_interface="org.freedesktop.NetworkManager" ) check_device_listener() bus.add_signal_receiver( nm_state_change, signal_name="StateChanged", dbus_interface="org.freedesktop.NetworkManager" ) nm_state_change(nm.nm_state()) def init_nmmon(): set_nm_listeners() def main(): handler = logging.StreamHandler(stream=None) log.addHandler(handler) log.setLevel(logging.DEBUG) log.info('starting') init_nmmon() def up(): print("wifi up") def down(): print("wifi down") set_device_callbacks('HOTSPOT', up, down) loop = MainLoop() loop.run() if __name__ == '__main__': main() comitup-master-1.2.3/comitup/persist.py000066400000000000000000000035471324443600300202210ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import os import json from functools import wraps class persist(dict): """A JSON-file backed persistent dictionary""" def __init__(self, path, *args, **kwargs): """Initialize with backing file path, and optional dict defaults""" super(persist, self).__init__(*args, **kwargs) self.__dict__['path'] = path if os.path.exists(self.path): self.load() self.save() def save(self): with open(self.path, 'w') as fp: json.dump(self, fp, indent=2) def load(self): with open(self.path, 'r') as fp: dict = json.load(fp) self.update(dict) def addsave(fn): @wraps(fn) def wrapper(inst, *args, **kwargs): # give wrapped function a chance to validate arguments fn(inst, *args, **kwargs) super_method = getattr(inst.__class__.__bases__[0], fn.__name__) retval = super_method(inst, *args, **kwargs) inst.save() return retval return wrapper @addsave def __setitem__(self, key, value, super_ret=None): pass @addsave def update(self, *args, **kwargs): pass @addsave def setdefault(self, *args, **kwargs): pass def __setattr__(self, name, value): if name in self.__dict__: self.__dict__[name] = value else: self.__setitem__(name, value) def __getattr__(self, name): if name in self.__dict__: return self.__dict__[name] else: return self.__getitem__(name) comitup-master-1.2.3/comitup/statemgr.py000077500000000000000000000103771324443600300203600ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import dbus import dbus.service import logging from comitup import iwscan import os import subprocess import sys sys.path.append("/usr/share/comitup") import pkg_resources # noqa from gi.repository.GLib import MainLoop # noqa import time # noqa from dbus.mainloop.glib import DBusGMainLoop # noqa DBusGMainLoop(set_as_default=True) from comitup import states # noqa from comitup import nm # noqa from comitup import modemgr # noqa comitup_path = "/com/github/davesteele/comitup" comitup_int = "com.github.davesteele.comitup" log = logging.getLogger('comitup') com_obj = None conf = None data = None apcache = None cachetime = 0 class Comitup(dbus.service.Object): def __init__(self): bus_name = dbus.service.BusName(comitup_int, bus=dbus.SystemBus()) dbus.service.Object.__init__(self, bus_name, comitup_path) @dbus.service.method(comitup_int, in_signature="", out_signature="aa{ss}") def access_points(self): global apcache, cachetime if time.time() - cachetime > 5: cachetime = time.time() # keep anyone else from processing aps = iwscan.candidates() aps = [x for x in aps if x['ssid'] != states.hotspot_name] apcache = aps # set a timeout, if we got something if len(apcache): cachetime = time.time() # cache time actually starts now else: cachetime = 0 return apcache @dbus.service.method(comitup_int, in_signature="", out_signature="ss") def state(self): return [states.com_state, states.connection] @dbus.service.method(comitup_int, in_signature="ss", out_signature="") def connect(self, ssid, password): if nm.get_connection_by_ssid(ssid): nm.del_connection_by_ssid(ssid) nm.make_connection_for(ssid, password) states.set_state('CONNECTING', [ssid, ssid]) @dbus.service.method(comitup_int, in_signature="", out_signature="") def delete_connection(self): ssid = nm.get_active_ssid(modemgr.get_link_device()) nm.del_connection_by_ssid(ssid) states.set_state('HOTSPOT') @dbus.service.method(comitup_int, in_signature="", out_signature="a{ss}") def get_info(self): info = { 'version': pkg_resources.get_distribution("comitup").version, 'basename': conf.base_name, 'id': data.id, 'hostnames': ';'.join(get_hosts(conf, data)), } return info def get_hosts(conf, data): return [ "%s-%s.local" % (conf.base_name, data.id), ] def external_callback(state, action): if action != 'start': return script = conf.external_callback if not os.path.isfile(script): return if not os.access(script, os.X_OK): log.error("Callback script %s is not executable" % script) return def demote(uid, gid): def dodemote(): os.setuid(uid) os.setgid(gid) return dodemote stats= os.stat(script) with open(os.devnull, 'w') as nul: subprocess.call( [script, state], stdout=nul, stderr=subprocess.STDOUT, preexec_fn=demote(stats.st_uid, stats.st_gid), ) def init_state_mgr(gconf, gdata, callbacks): global com_obj, conf, data conf, data = (gconf, gdata) states.init_states(get_hosts(conf, data), callbacks + [external_callback]) com_obj = Comitup() states.set_state('HOTSPOT', timeout=5) def main(): handler = logging.StreamHandler(stream=None) log.addHandler(handler) log.setLevel(logging.DEBUG) log.info('starting') init_state_mgr('comitup.local', 'comitup-1111.local') states.set_state('HOTSPOT', timeout=5) loop = MainLoop() loop.run() if __name__ == '__main__': main() comitup-master-1.2.3/comitup/states.py000066400000000000000000000207241324443600300200270ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import logging import time from functools import wraps from dbus.exceptions import DBusException from comitup import iwscan from gi.repository.GLib import MainLoop, timeout_add if __name__ == '__main__': from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) from comitup import nmmon # noqa from comitup import nm # noqa from comitup import mdns # noqa from comitup import modemgr # noqa log = logging.getLogger('comitup') # definitions dns_names = [] # Global state information com_state = None conn_list = [] connection = '' state_id = 0 points = [] state_callbacks = [] hotspot_name = None def state_callback(fn): @wraps(fn) def wrapper(*args, **kwargs): returnvalue = fn(*args, **kwargs) state, action = fn.__name__.split('_') state = state.upper() for callback in state_callbacks: callback(state, action) return returnvalue return wrapper def timeout(fn): @wraps(fn) def wrapper(id): if id == state_id: fn() return True else: return False return wrapper def dns_to_conn(host): if '.local' in host: return host[:-len('.local')] else: return host # # Hotspot state # def fake_hs_pass(): hotspot_pass() return False @state_callback def hotspot_start(): global conn_list log.info("Activating hotspot") hs_ssid = dns_to_conn(dns_names[0]) # if we are in two-wifi device mode, skip the reconnect if possible, # to avoid kicking some clients off log.debug("states: Calling nm.get_active_ssid()") if hs_ssid != nm.get_active_ssid(modemgr.get_state_device('HOTSPOT')): mdns.clear_entries() conn_list = [] # tolerate Raspberry Pi 2 try: activate_connection(hs_ssid, 'HOTSPOT') except DBusException: log.warn("Error connecting hotspot") else: log.debug("Didn't need to reactivate - already running") # the connect callback won't happen - let's 'pass' manually timeout_add(100, fake_hs_pass) @state_callback def hotspot_pass(): log.debug("Activating mdns") # IP tolerance for PI 2 for _ in range(5): log.debug("states: Calling nm.get_active_ip()") ip = nm.get_active_ip(modemgr.get_state_device('HOTSPOT')) if ip: mdns.clear_entries() mdns.add_hosts(dns_names) break time.sleep(1) @state_callback def hotspot_fail(): log.warn("Hotspot mode failure") pass @timeout def hotspot_timeout(): if iwscan.ap_conn_count() == 0 or modemgr.get_mode() != 'single': log.debug('Periodic connection attempt') dev = modemgr.get_state_device('CONNECTED') conn_list = candidate_connections(dev) if conn_list: # bug - try the first connection twice set_state('CONNECTING', [conn_list[0], conn_list[0]] + conn_list) else: set_state('CONNECTING') else: log.info('AP active - skipping CONNECTING scan') # # Connecting state # @state_callback def connecting_start(): global conn_list mdns.clear_entries() if conn_list: log.debug("states: Calling nm.disconnect()") nm.disconnect(modemgr.get_state_device('CONNECTING')) conn = conn_list.pop(0) log.info('Attempting connection to %s' % conn) activate_connection(conn, 'CONNECTING') else: # Give NetworkManager a chance to update the access point list try: # todo - clean this up log.debug("states: Calling nm.deactivate_connection()") nm.deactivate_connection(modemgr.get_state_device('CONNECTING')) except DBusException: pass time.sleep(5) set_state('HOTSPOT') @state_callback def connecting_pass(): log.debug("Connection successful") set_state('CONNECTED') @state_callback def connecting_fail(): log.debug("Connection failed") if conn_list: set_state('CONNECTING') else: set_state('HOTSPOT') @timeout def connecting_timeout(): connecting_fail() # # Connect state # @state_callback def connected_start(): global conn_list # IP tolerance for PI 2 for _ in range(5): log.debug("states: Calling nm.get_active_ip()") ip = nm.get_active_ip(modemgr.get_state_device('CONNECTED')) if ip: mdns.clear_entries() mdns.add_hosts(dns_names) break time.sleep(1) conn_list = [] @state_callback def connected_pass(): pass @state_callback def connected_fail(): log.warn('Connection lost') set_state('HOTSPOT') @timeout def connected_timeout(): log.debug("states: Calling nm.get_active_ssid()") if connection != nm.get_active_ssid(modemgr.get_state_device('CONNECTED')): log.warn("Connection lost on timeout") set_state('HOTSPOT') # # State Management # class state_matrix(object): """Map e.g. state_matrix('HOTSPOT').pass_fn to the function hotspot_pass""" def __init__(self, state): self.state = state.lower() def __getattr__(self, attr): try: fname = self.state + '_' + attr[:-3] return globals()[fname] except KeyError: print(attr) raise AttributeError def set_state(state, connections=None, timeout=180): global com_state, conn_list, state_id, points log.info('Setting state to %s' % state) if com_state != 'HOTSPOT': log.debug("states: Calling nm.get_points_ext()") points = nm.get_points_ext(modemgr.get_state_device(com_state)) state_info = state_matrix(state) nmmon.set_device_callbacks(state, state_info.pass_fn, state_info.fail_fn) if connections: conn_list = connections state_id += 1 com_state = state state_info.start_fn() timeout_add(timeout*1000, state_info.timeout_fn, state_id) def activate_connection(name, state): global connection connection = name log.debug('Connecting to %s' % connection) try: path = [x['nmpath'] for x in points if x['ssid'] == name][0] except IndexError: path = '/' log.debug("states: Calling nm.activate_connection_by_ssid()") nm.activate_connection_by_ssid(connection, modemgr.get_state_device(state), path=path) def candidate_connections(device): log.debug("states: Calling nm.get_candidate_connections()") return nm.get_candidate_connections(device) def set_hosts(*args): global dns_names dns_names = args def assure_hotspot(ssid, device): log.debug("states: Calling nm.get_connection_by_ssid()") if not nm.get_connection_by_ssid(ssid): nm.make_hotspot(ssid, device) def state_monitor(): # NetworkManager is crashing on the first call to attach. In two # interface mode, this leaves the hotspot interface with no IP # configuration. Detect this and recover if com_state != 'HOTSPOT': if modemgr.get_mode() == modemgr.MULTI_MODE: log.debug("state_monitor: Calling nm.get_active_ip()") ip = nm.get_active_ip(modemgr.get_state_device('HOTSPOT')) if not ip: log.warn("Hotspot lost IP configuration - resetting") hs_ssid = dns_to_conn(dns_names[0]) activate_connection(hs_ssid, 'HOTSPOT') # Keep this periodic task running return True def init_states(hosts, callbacks): global hotspot_name nmmon.init_nmmon() set_hosts(*hosts) for callback in callbacks: add_state_callback(callback) hotspot_name = dns_to_conn(hosts[0]) assure_hotspot(hotspot_name, modemgr.get_ap_device()) timeout_add(10*1000, state_monitor) def add_state_callback(callback): global state_callbacks state_callbacks.append(callback) if __name__ == '__main__': handler = logging.StreamHandler(stream=None) log.addHandler(handler) log.setLevel(logging.DEBUG) log.info("Starting") init_states('comitup.local', 'comitup-1111.local') set_state('HOTSPOT') # set_state('CONNECTING', candidate_connections()) loop = MainLoop() loop.run() comitup-master-1.2.3/comitup/webmgr.py000066400000000000000000000031431324443600300200030ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import logging import dbus bus = dbus.SystemBus() systemd_service = bus.get_object( 'org.freedesktop.systemd1', '/org/freedesktop/systemd1', ) sd_start_unit = systemd_service.get_dbus_method( 'StartUnit', 'org.freedesktop.systemd1.Manager', ) sd_stop_unit = systemd_service.get_dbus_method( 'StopUnit', 'org.freedesktop.systemd1.Manager', ) log = logging.getLogger('comitup') COMITUP_SERVICE = 'comitup-web.service' web_service = "" def start_service(service): log.debug("starting %s web service", service) sd_start_unit(service, 'replace') def stop_service(service): log.debug("stopping %s web service", service) sd_stop_unit(service, 'replace') callmatrix = { ('HOTSPOT', 'start'): (lambda: stop_service, lambda: web_service), ('HOTSPOT', 'pass'): (lambda: start_service, lambda: COMITUP_SERVICE), ('CONNECTING', 'pass'): (lambda: stop_service, lambda: COMITUP_SERVICE), ('CONNECTED', 'start'): (lambda: start_service, lambda: web_service), } def state_callback(state, action): try: (fn_fact, svc_fact) = callmatrix[(state, action)] except KeyError: return if svc_fact(): fn_fact()(svc_fact()) def callback_target(): return state_callback def init_webmgr(web_svc): global web_service web_service = web_svc comitup-master-1.2.3/comitup/wificheck.py000077500000000000000000000056311324443600300204630ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE from collections import namedtuple import textwrap import logging import subprocess import re log = logging.getLogger('comitup') def device_present(): try: if subprocess.check_output("iw list".split()).decode() == "": # Fail without comment return "" return None except CalledProcessError: return "" def device_supports_ap(): phy_txt = subprocess.check_output("iw list".split()).decode() phy_lst = [x.split()[1] for x in phy_txt.split('\n') if "Wiphy " in x] phy = sorted(phy_lst)[0] cmd = "iw phy {} info".format(phy) deviceinfo = subprocess.check_output(cmd.split()).decode() if "* AP\n" not in deviceinfo: return phy return None def device_nm_managed(): devicesinfo = subprocess.check_output("nmcli device show".split(), re.MULTILINE).decode() if not re.search("GENERAL.TYPE\W+wifi", devicesinfo): # Fail without comment return "" return None testspec = namedtuple('testspec', ['testfn', 'title', 'description']) testspecs = [ testspec( device_present, "comitup-no-wifi - No wifi devices found", textwrap.dedent(""" Comitup is a wifi device manager. 'sudo iw list' indicates that there are no devices to manage. """), ), testspec( device_supports_ap, "comitup-no-ap - The Main wifi device doesn't support AP mode", textwrap.dedent(""" Comitup uses the first wifi device to implement the comitup- Access Point. For this to work, the device must include "AP" in list of "Supported interface modes" returned by "iw list". """), ), testspec( device_nm_managed, "comitup-no-nm - Wifi device is not managed by NetworkManager", textwrap.dedent(""" Comitup uses NetworkManager to manage the wifi devices, but the required devices are not listed. This usually means that the devices are listed in /etc/network/interfaces, and are therefore being managed elsewhere. Remove the references to wifi devices from that file. """), ), ] def run_checks(logit=True, printit=True, verbose=True): for testspec in testspecs: testresult = testspec.testfn() if testresult is not None: if logit: log.error(testspec.title) if testresult: log.error(" " + testresult) if printit: print(testspec.title) if testresult: print(" " + testresult) if verbose: print(testspec.description) return True return None if __name__ == '__main__': run_checks(logit=False) comitup-master-1.2.3/conf/000077500000000000000000000000001324443600300154125ustar00rootroot00000000000000comitup-master-1.2.3/conf/comitup-dbus.conf000066400000000000000000000006641324443600300207020ustar00rootroot00000000000000 comitup-master-1.2.3/conf/comitup.conf000066400000000000000000000021361324443600300177430ustar00rootroot00000000000000# # Comitup configuration # # base_name # # Comitup will publish a pair of ZeroConf host names: 'comitup.local' # and 'comitup-.local', where is a 4-digit number. This # parameter allows the base 'comitup' name to be changed. # # base_name: comitup # web_service # # The name of a systemd service to be disabled when comitup is managing a # hotspot, and enabled when there is a normal wifi connection. # # web_service: httpd.service # enable_appliance_mode # # If enabled (true), and if two wifi adapters are available, comitup will # maintain the comitup- hotspot on the first, and make other AP # connections on the second. IP forwarding and NAT are enabled, so that # hosts on the comitup hotspot will be able to access external networks. # # enable_appliance_mode: true # external_callback # # An external script that is called on comitup state changes. It will # include a single argument, either 'HOTSPOT', 'CONNECTING', or # 'CONNECTED'. # # The script must be executable. It will be run with the permissions of the # owning user. # # external_callback: /usr/local/bin/comitup-callback comitup-master-1.2.3/conf/comitup.json000066400000000000000000000000031324443600300177560ustar00rootroot00000000000000{} comitup-master-1.2.3/doc/000077500000000000000000000000001324443600300152325ustar00rootroot00000000000000comitup-master-1.2.3/doc/comitup-cli.1.ronn000066400000000000000000000051761324443600300205250ustar00rootroot00000000000000comitup-cli(1) -- command-line interface for comitup network management ============================================= ## SYNOPSIS $ `comitup-cli` State: HOTSPOT Connection: hotspot-1234 Points: 1: MyAccessPoint 2: HisAccessPoint Available Commands: (r)eload (q)uit connect to () command?: ## DESCRIPTION The **comitup-cli** utility provides access to the comitup(8) D-Bus interface. It is intended to serve as a debug tool, and a source code example for connecting to the interface. If the comitup(8) service is not running, **comitup-cli** will immediately exit. Display: * **State** The **comitup** states are **HOTSPOT**, **CONNECTING**, and **CONNECTED**. In the **HOTSPOT** mode, **comitup** creates a wifi hotspot with the name **comitup-<nnnn>**, where <nnnn> is a persistent 4-digit number. Once in **HOTSPOT** mode, the system will occasionally (~1/min) cycle through available defined connections, by transitioning to the **CONNECTING** mode. The Access Point list is updated by this process. Any command issued by **comitup-cli** will cause the next timeout instance to be skipped. Once a connection is established, the system will be in the **CONNECTED** mode. If the connection is lost, failed, or deleted, the system will transition back to the **HOTSPOT** state. * **Connection** The `ssid` of the current active connection. * **Points** While in the **HOTSPOT** mode, **comitup-cli** will list the currently-visible access points, by `ssid`. Access points with the strongest signal are sorted to the top of the list. The entries are numbered, for use with the __connect__ command. Commands: * __r__ - **Reload** Refresh the displayed state, mode, and list of access points. * __q__ - **Quit** Exit **comitup.cli**. * __d__ - **Delete connection** Delete the configuration information for the current wifi connection. This will cause **comitup** to transition to the **HOTSPOT** mode. This command is not available in the **HOTSPOT** mode. * __<n>__ - **Connect to access point <n>** Define a connection for the selected access point, and then attempt to connect. This command is only available in the **HOTSPOT** mode. * __m__ - **Manual connection** Enter an SSID manually. * __i__ - **Get information** Return the comitup version and host name for the current instance. ## COPYRIGHT Comitup is Copyright (C) 2016-2017 David Steele <steele@debian.org> ## SEE ALSO comitup(8), comitup-conf(5), comitup-web(8) comitup-master-1.2.3/doc/comitup-conf.5.ronn000066400000000000000000000030771324443600300207050ustar00rootroot00000000000000comitup.conf(5) -- Comitup configuration file format ============================================= ## DESCRIPTION The _comitup.conf_ file configures the wifi management service comitup(8). It is located in the _/etc/_ directory. ## PARAMETERS * _base_name_: By default, comitup will create a hotspot named **comitup-<nnnn>**, and publish avahi-daemon(8) host entries for **comitup-<nnnn>** and **comitup**. Setting this entry will change the **comitup** string with one of the users choosing. * _web_service_: This defines a user web service to be controlled by **comitup**. This service will be disabled in the **HOTSPOT** state in preference of the comitup web service, and will be enabled in the **CONNECTED** state. This should be the name of the systemd web service, such as _apache2.service_ or _nginx.service_. This defaults to a null string, meaning no service is controlled. * _enable_appliance_mode_: By default, comitup will use multiple wifi interfaces, if available, to connect to the local hotspot and to the internet simultaneously. Setting this to something other than "true" will limit comitup to the first wifi interface. * _external_callback_: The path to an external script that is called on comitup state changes. It will include a single argument, either 'HOTSPOT', 'CONNECTING', or 'CONNECTED'. The script will run as the owning user and group. ## COPYRIGHT Comitup is Copyright (C) 2016-2017 David Steele <steele@debian.org> ## SEE ALSO comitup(8), comitup-cli(1), comitup-web(8) comitup-master-1.2.3/doc/comitup-web.8.ronn000066400000000000000000000006701324443600300205340ustar00rootroot00000000000000comitup-web(8) -- Wifi configuration web server for the Comitup service ============================================= ## SYNOPSIS `comitup-web` ## DESCRIPTION __comitup-web__ is a web service that provides access to the comitup(8) Wifi bootstrap service. ## OPTIONS See comitup-conf(5). ## COPYRIGHT Comitup is Copyright (C) 2016-2017 David Steele <steele@debian.org> ## SEE ALSO comitup(8), comitup-conf(5), comitup-cli(1) comitup-master-1.2.3/doc/comitup.8.ronn000066400000000000000000000065221324443600300177630ustar00rootroot00000000000000comitup(8) -- Manage wifi connections on headless, unconnected systems ============================================= ## SYNOPSIS `comitup [options]` ## DESCRIPTION The **comitup** service provides a means to establish a connection between a computer and a wifi access point, in the case where wifi is the only means available to access the computer. On startup, the service will attempt to connect to wifi using established networkmanager(8) connections. If this is not successful, **comitup** will establish a wifi hotspot with the name _comitup-<nnnn>_, where <nnnn> is a persistent 4-digit number. While the hotspot is active, a comitup-web(8) service is available to manage connecting to an access point. If two wifi interfaces are available, the hotspot will remain active on the first interface, and the internet connection will be made on the second. Otherwise, the hotspot will be replaced with the internet connection. In all states, avahi-daemon(8) is used to publish the mdns host name _comitup-<nnnn>.local_, making the web service accessible as e.g. _http://comitup-1234.local_, for systems supporting Zeroconf. For other systems, a _comitup_ Workstation entry is published which is visible to Zeroconf browsing applications, allowing the IP address to be manually determined. The web service address is likely _http://10.42.0.1_. **comitup** logs to _/var/log/comitup.log_. ## Options * _-h_, _--help_ - Print help and exit * _-c_, _--check_ - Check the wifi device configuration and exit ## D-Bus Interface **Comitup** provides a D-Bus object which claims the name _com.github.davesteele.comitup_ on the path _/com/github/davesteele/comitup_, supporting the interface _com.github.davesteele.comitup_. The interface includes the following methods. * _get_info()_ Input: None Output: _DICT_ENTRY_ Return information about the current **Comitup** service. The keys are as follows: * _version_ - The package version. * _basename_ - The currently configured basename (default **comitup**). * _id_ - The unique random numeric id associated with the service instance. * _hostnames_ - A list of host names that are published for the service IP address. * _access_points()_ Input: None Output: Array of _DICT_ENTRY_ Return a list of visible access points. This is represented as an array of D-Bus _DICT_ENTRY_. Each _DICT_ENTRY_ contains strings associated with the following keys, _ssid_, _strength_ (0 to 100) and _security_ (_encrypted_ or _unencrypted_). * _state()_ Input: None Output: _state_, _connection_ This returns strings for the current **comitup** state (either **HOTSPOT**, **CONNECTING**, or **CONNECTED**) and the _ssid_ name for the current connection on the wifi device. * _connect()_ Input: _ssid_, _password_ Output: None Delete any existing connection for _ssid_, create a new connection, and attempt to connect to it. The password may be a zero length string if not needed. * _delete_connection()_ Input: _ssid_ Output: None Delete the connection for _ssid_. The system will not be able to reconnect using this connection. ## COPYRIGHT Comitup is Copyright (C) 2016-2017 David Steele <steele@debian.org>. ## SEE ALSO comitup-conf(5), comitup-cli(1), comitup-web(8) comitup-master-1.2.3/doc/index.txt000066400000000000000000000005341324443600300171040ustar00rootroot00000000000000comitup-conf(5) comitup-conf.5.ronn comitup(8) comitup.8.ronn comitup-web(8) comitup-web.8.ronn comitup-cli(1) comitup-cli.1.ronn networkmanager(8) http://linux.die.net/man/8/networkmanager avahi-daemon(8) http://man.cx/avahi-daemon(8) Zeroconf https://en.wikipedia.org/wiki/Zero-configuration_networking comitup-master-1.2.3/setup.py000066400000000000000000000056751324443600300162140ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # from setuptools import setup from distutils.command.clean import clean from setuptools.command.test import test import os import shutil import sys class PyTest(test): user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] def initialize_options(self): test.initialize_options(self) self.pytest_args = [] def finalize_options(self): test.finalize_options(self) def run_tests(self): import pytest errno = pytest.main(self.pytest_args) sys.exit(errno) class MyClean(clean): def run(self): clean.run(self) for root, dirs, files in os.walk('.'): [shutil.rmtree(os.path.join(root, x)) for x in dirs if x in (".pyc", ".coverage", ".cache", "__pycache__", "comitup.egg-info")] for file in files: for match in (".pyc", ".cache", ".coverage"): if match in file: os.unlink(os.path.join(root, file)) setup( name='comitup', packages=['comitup', 'web', 'cli'], version='1.2.3', description="Remotely manage wifi connections on a headless computer", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Flask', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved ' + ':: GNU General Public License v2 or later (GPLv2+)', 'Natural Language :: English', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Topic :: System :: Networking', ], entry_points={ 'console_scripts': [ 'comitup=comitup.comitup:main', 'comitup-cli=cli.comitupcli:interpreter', 'comitup-web=web.comitupweb:main', ], }, options={ 'build_scripts': { 'executable': '/usr/bin/python3', }, }, data_files=[ ('/etc', ['conf/comitup.conf']), ('/var/lib/comitup', ['conf/comitup.json']), ('/etc/dbus-1/system.d', ['conf/comitup-dbus.conf']), ('/usr/share/comitup/web', ['web/comitupweb.conf']), ('/usr/share/comitup/web/templates', [ 'web/templates/index.html', 'web/templates/connect.html', 'web/templates/confirm.html', ] ), # noqa ], install_requires=[ 'jinja2', ], tests_require=['pytest', 'mock'], cmdclass={ 'clean': MyClean, 'test': PyTest, }, author="David Steele", author_email="steele@debian.org", url='https://davesteele.github.io/comitup/', ) comitup-master-1.2.3/test/000077500000000000000000000000001324443600300154445ustar00rootroot00000000000000comitup-master-1.2.3/test/__init__.py000066400000000000000000000000001324443600300175430ustar00rootroot00000000000000comitup-master-1.2.3/test/test_comitup.py000066400000000000000000000033021324443600300205330ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import pytest from mock import Mock, patch import textwrap import os from comitup import comitup as ciu @pytest.fixture() def conf_fxt(tmpdir, monkeypatch): path = os.path.join(tmpdir.__str__(), 'conffile') open(path, 'w').write(textwrap.dedent( """ base_name: test """ )) monkeypatch.setattr('comitup.comitup.CONF_PATH', path) return path @pytest.fixture() def persist_fxt(tmpdir, monkeypatch): path = os.path.join(tmpdir.__str__(), 'persistfile') monkeypatch.setattr('comitup.comitup.PERSIST_PATH', path) monkeypatch.setattr( 'comitup.comitup.random.randrange', Mock(return_value=1234) ) return path @pytest.fixture() def log_fxt(tmpdir, monkeypatch): path = os.path.join(tmpdir.__str__(), 'logfile') monkeypatch.setattr('comitup.comitup.LOG_PATH', path) return path def test_ciu_deflog(log_fxt): log = ciu.deflog() log.info('foo') txt = open(log_fxt, 'r').read() assert 'INFO' in txt assert 'foo' in txt assert int(txt[:4]) def test_ciu_loadconf(conf_fxt, persist_fxt): (conf, data) = ciu.load_data() assert conf.base_name == 'test' assert os.path.isfile(persist_fxt) def test_ciu_inst_name(conf_fxt, persist_fxt): (conf, data) = ciu.load_data() assert ciu.inst_name(conf, data) == 'test-1234' @pytest.fixture() def loop_fxt(monkeypatch): loop = Mock() monkeypatch.setattr( 'comitup.comitup.MainLoop', Mock(return_value=loop) ) return loop comitup-master-1.2.3/test/test_config.py000066400000000000000000000015501324443600300203230ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import pytest import textwrap import os from comitup import config @pytest.fixture() def conf_fxt(tmpdir): path = os.path.join(tmpdir.__str__(), "conf.conf") with open(path, 'w') as fp: fp.write( textwrap.dedent( """ # # comment # tag1=val1 tag2: val2 tag3 = val3 # tag4 = val4 """ ) ) return config.Config(path) @pytest.mark.parametrize("idx", ('1', '2', '3')) def test_conf_vals(idx, conf_fxt): assert eval('conf_fxt.tag{0} == "val{0}"'.format(idx)) def test_conf_miss(conf_fxt): with pytest.raises(AttributeError): conf_fxt.tag4 comitup-master-1.2.3/test/test_mdns.py000066400000000000000000000035661324443600300200300ustar00rootroot00000000000000import pytest # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE from comitup import mdns from mock import Mock, patch @pytest.fixture() def avahi_fxt(monkeypatch, request): monkeypatch.setattr("comitup.mdns.dbus.Interface", Mock()) monkeypatch.setattr('comitup.mdns.dbus.SystemBus', Mock()) monkeypatch.setattr('comitup.mdns.log', Mock()) save_group = mdns.group mdns.group = Mock() def fin(): mdns.group = save_group request.addfinalizer(fin) return None def test_avahi_null(avahi_fxt): pass def test_avahi_establish_group(avahi_fxt): old_group = mdns.group mdns.establish_group() assert mdns.group != old_group def test_avahi_make_a_record(avahi_fxt): mdns.make_a_record('host', '1', '1.2.3.4') assert mdns.group.AddRecord.called def test_avahi_add_service(avahi_fxt): mdns.add_service('host', '1', '1.2.3.4') assert mdns.group.AddService.called @patch('comitup.mdns.establish_group', Mock()) def test_avahi_clear_entries(avahi_fxt): isempty = Mock(return_value=False) mdns.group = Mock() mdns.group.IsEmpty = isempty mdns.clear_entries() assert isempty.called assert mdns.group.Reset.called assert mdns.establish_group.called assert not mdns.log.called @patch('comitup.nm.get_devices', Mock(return_value=[])) def test_avahi_add_hosts(avahi_fxt): mdns.add_hosts(['host1', 'host2']) assert mdns.group.Commit.called @pytest.mark.parametrize("dns_in, dns_out", ( ("a.b.c", "a.b.c".encode()), ("A.B.C", "A.B.C".encode()), ("a..b", "a.b".encode()), ("a.b.", "a.b".encode()), )) def test_avahi_encode_dns(dns_in, dns_out): assert dns_out == mdns.encode_dns(dns_in) @patch('comitup.mdns.log.warn') def test_avahi_clear_fail(warn, avahi_fxt): mdns.group = None mdns.clear_entries() comitup-master-1.2.3/test/test_nm.py000066400000000000000000000071301324443600300174700ustar00rootroot00000000000000import pytest # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE from mock import Mock, patch from comitup import nm @pytest.fixture() def no_device_fxt(monkeypatch): monkeypatch.setattr("comitup.nm.nm.NetworkManager.GetDevices", Mock(return_value=[])) return None @pytest.fixture() def device_no_conn_fxt(monkeypatch): device = Mock() device.ActiveConnection = None monkeypatch.setattr("comitup.nm.nm.NetworkManager.GetDevices", Mock(return_value=[device])) return None @pytest.fixture() def device_fxt(monkeypatch): device = Mock() device.DeviceType = 2 device.ActiveConnection.Connection.GetSettings.return_value = { '802-11-wireless': { 'ssid': "myssid", } } device.Ip4Config.Addresses = [['1.2.3.4', '5.6.7.8', '1.2.3.1']] point = Mock() point.Ssid = "myssid" point.Strength = 100 getAllAccessPoints = Mock() getAllAccessPoints.GetAllAccessPoints.return_value = [point] device.SpecificDevice.return_value = getAllAccessPoints monkeypatch.setattr("comitup.nm.nm.NetworkManager.GetDevices", Mock(return_value=[device])) return None @pytest.fixture() def no_connections_fxt(monkeypatch): monkeypatch.setattr("comitup.nm.nm.Settings.ListConnections", Mock(return_value=[])) return None @pytest.fixture() def connections_fxt(monkeypatch): connection = Mock() connection.GetSettings.return_value = { '802-11-wireless': { 'ssid': "myssid", } } monkeypatch.setattr("comitup.nm.nm.Settings.ListConnections", Mock(return_value=[connection])) return connection @pytest.mark.parametrize("func", ( nm.get_wifi_device, nm.get_active_ssid, nm.get_active_ip, nm.get_access_points, ) ) def test_none_dev(no_device_fxt, func): if func is nm.get_wifi_device: assert func(0) is None else: assert func(nm.get_wifi_device()) is None def test_no_active_ssid(device_no_conn_fxt): assert nm.get_active_ssid(nm.get_wifi_device()) is None def test_get_active_ssid(device_fxt): assert nm.get_active_ssid(nm.get_wifi_device()) == "myssid" def test_get_active_ip(device_fxt): assert nm.get_active_ip(nm.get_wifi_device()) == '1.2.3.4' def test_no_conn(no_connections_fxt): assert nm.get_connection_by_ssid('ssid') is None def test_get_connection_by_ssid(connections_fxt): assert nm.get_connection_by_ssid("myssid") assert not nm.get_connection_by_ssid("bogusssid") def test_del_connection_by_ssid(connections_fxt): nm.del_connection_by_ssid("myssid") assert connections_fxt.Delete.called @patch('comitup.nm.get_wifi_device') def test_activate_connection_by_id(get_dev, monkeypatch, connections_fxt): activate = Mock() monkeypatch.setattr("comitup.nm.nm.NetworkManager.ActivateConnection", activate) nm.activate_connection_by_ssid("myssid", nm.get_wifi_device()) assert activate.called def test_make_hotspot(monkeypatch): addconnection = Mock() monkeypatch.setattr("comitup.nm.nm.Settings.AddConnection", addconnection) nm.make_hotspot() assert addconnection.called def test_make_connection_for(monkeypatch): addconnection = Mock() monkeypatch.setattr("comitup.nm.nm.Settings.AddConnection", addconnection) nm.make_connection_for("anssid", "password") assert addconnection.called def test_do_listaccess(device_fxt): nm.do_listaccess(None) comitup-master-1.2.3/test/test_nmmon.py000066400000000000000000000052101324443600300201770ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import pytest from mock import Mock, patch from comitup import nmmon @pytest.fixture() def nmmon_con_fxt(request): def fin(): nmmon.nm_dev_connect = nmmon.null_fn nmmon.nm_dev_fail = nmmon.null_fn request.addfinalizer(fin) def test_nmmon_null_init(): nmmon.nm_dev_connect() nmmon.nm_dev_fail() nmmon.null_fn() @patch('comitup.nm.get_device_path', return_value='/') def test_nmmon_set_callbacks(nmmon_con_fxt): nmmon.set_device_callbacks('HOTSPOT', 1, 2) assert nmmon.nm_dev_connect == 1 assert nmmon.nm_dev_fail == 2 @pytest.mark.parametrize("state, pass_called, fail_called", ( (90, False, False), (100, True, False), (120, False, True), )) @patch('comitup.nmmon.nm_dev_connect') @patch('comitup.nmmon.nm_dev_fail') def test_nmmon_dev_change(fail_fn, connect_fn, state, pass_called, fail_called): nmmon.nm_device_change(state) assert connect_fn.called == pass_called assert fail_fn.called == fail_called @pytest.fixture() def bus_fxt(request): bus_save = nmmon.bus nmmon.bus = Mock() def fin(): nmmon.bus = bus_save request.addfinalizer(fin) return nmmon.bus.add_signal_receiver @patch('comitup.nmmon.nm.nm_state', return_value=0) @patch('comitup.nmmon.check_device_listener') def test_nmmon_set_listener(check_listener, nm_state, bus_fxt): nmmon.set_device_listener('apath') assert bus_fxt.called @patch('comitup.nmmon.nm.get_device_path', return_value='somepath') @patch('comitup.nmmon.set_device_listener') def test_nmmon_check_listener(listener, dev_path, bus_fxt, devpath_fxt): nmmon.check_device_listener() assert listener.called @pytest.fixture() def devpath_fxt(request): dev_save = nmmon.device_path def fin(): nmmon.device_path = dev_save request.addfinalizer(fin) @pytest.mark.parametrize('state, called', ((0, False,), (100, True))) @patch('comitup.nmmon.check_device_listener') def test_nmmon_state_change(chk_listen, bus_fxt, devpath_fxt, state, called): nmmon.nm_state_change(state) assert chk_listen.called == called @patch('comitup.nmmon.check_device_listener') def test_nmmon_set_nm_listeners(check_listener, bus_fxt): nmmon.set_nm_listeners() assert bus_fxt.called assert check_listener.called @patch('comitup.nmmon.check_device_listener') def test_nmmon_init(check_listener, bus_fxt): nmmon.init_nmmon() assert bus_fxt.called comitup-master-1.2.3/test/test_persist.py000066400000000000000000000050311324443600300205450ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE from comitup.persist import persist import pytest import os import tempfile import shutil @pytest.fixture(scope='module') def dir_fxt(request): dir = tempfile.mkdtemp() def fin(): shutil.rmtree(dir) request.addfinalizer(fin) return dir count = 0 @pytest.fixture() def jsonpath(request, dir_fxt): global count path = os.path.join(dir_fxt, 'persist%d.json' % count) count += 1 return path def test_persist_is_dict(jsonpath): mydict = persist(jsonpath) mydict['a'] = 'b' assert mydict['a'] == 'b' def test_persist_default(jsonpath): mydict = persist(jsonpath, {'a': 'b'}) assert mydict['a'] == 'b' def test_persist_default_persists(jsonpath): persist(jsonpath, {'a': 'b'}) new = persist(jsonpath) assert new['a'] == 'b' def test_persist_override_default(jsonpath): persist(jsonpath, {'a': 'b'}) new = persist(jsonpath, {'a': 'c'}) assert new['a'] == 'b' def test_persist_override_default2(jsonpath): mydict = persist(jsonpath, {'a': 'a'}) mydict['a'] = 'b' new = persist(jsonpath, {'a': 'c'}) assert new['a'] == 'b' def test_persist_update(jsonpath): mydict = persist(jsonpath, {'a': 'a'}) mydict.update({'a': 'b'}) new = persist(jsonpath, {'a': 'c'}) assert new['a'] == 'b' def test_persist_setdefault(jsonpath): mydict = persist(jsonpath) mydict.setdefault('a', 'b') new = persist(jsonpath, {'a': 'c'}) assert new['a'] == 'b' def test_persist_setattr(jsonpath): mydict = persist(jsonpath) mydict.a = 'b' assert mydict['a'] == 'b' def test_persist_getattr(jsonpath): mydict = persist(jsonpath) mydict['a'] = 'b' assert mydict.a == 'b' def test_persist_persist_setattr(jsonpath): mydict = persist(jsonpath) mydict.a = 'b' new = persist(jsonpath) assert new['a'] == 'b' def test_persist_persist_getattr(jsonpath): mydict = persist(jsonpath) mydict['a'] = 'b' new = persist(jsonpath) assert new.a == 'b' def test_persist_file_format(jsonpath): mydict = persist(jsonpath) mydict['a'] = 'b' expected = '{\n "a": "b"\n}' assert open(jsonpath, 'r').read() == expected def test_persist_get_attr_dict(jsonpath): mydict = persist(jsonpath) assert mydict.path == jsonpath assert mydict.__getattr__('path') == jsonpath def test_persist_set_attr_dict(jsonpath): mydict = persist(jsonpath) mydict.path = jsonpath comitup-master-1.2.3/test/test_statemgr.py000066400000000000000000000021571324443600300207100ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import pytest from mock import Mock import importlib import dbus.service def nullwrapper(*args, **kwargs): def _nullwrapper(fn): def wrapper(*wargs, **wkwargs): return fn(*wargs, **wkwargs) return wrapper return _nullwrapper dbus.service.method = nullwrapper sm = importlib.import_module('comitup.statemgr') @pytest.fixture() def statemgr_fxt(monkeypatch, request): monkeypatch.setattr('dbus.service.BusName', Mock()) monkeypatch.setattr('dbus.service.Object', Mock()) monkeypatch.setattr('dbus.service.Object', Mock()) save_state = sm.states.com_state save_conn = sm.states.connection def fin(): sm.states.com_state = save_state sm.states.connection = save_conn request.addfinalizer(fin) sm.states.com_state = "CONNECTED" sm.states.connection = 'connection' def test_sm_none(statemgr_fxt): pass def test_sm_state(statemgr_fxt): obj = sm.Comitup() assert obj.state() == ['CONNECTED', 'connection'] comitup-master-1.2.3/test/test_states.py000066400000000000000000000125631324443600300203670ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE from comitup import states import pytest from mock import Mock, patch, call @pytest.fixture() def state_globals(request): callbacks = states.state_callbacks def fin(): states.state_callbacks = callbacks request.addfinalizer(fin) @pytest.fixture() def state_fxt(monkeypatch, state_globals): monkeypatches = ( ('comitup.states.mdns.clear_entries', None), ('comitup.states.mdns.add_hosts', None), ('comitup.states.nm.activate_connection_by_ssid', None), ('comitup.states.nm.get_candidate_connections', ['c1', 'c2']), ('comitup.states.nm.get_active_ip', '1.2.3.4'), ('comitup.states.nm.get_active_ssid', None), ('comitup.states.nm.deactivate_connection', None), ('comitup.states.nmmon.init_nmmon', None), ('comitup.states.nmmon.set_device_callbacks', None), ('comitup.states.timeout_add', None), ('comitup.states.time.sleep', None), ) for path, return_val in monkeypatches: if return_val: monkeypatch.setattr(path, Mock(return_value=return_val)) else: monkeypatch.setattr(path, Mock()) monkeypatch.setattr('comitup.states.iwscan.ap_conn_count', Mock(return_value=0)) monkeypatch.setattr('comitup.states.modemgr.CONF_PATH', '/dev/null') states.set_hosts('hs', 'hs-1111') @pytest.fixture() def points_fxt(monkeypatch): pt = { 'nmpath': '/', 'ssid': 'ssid', } monkeypatch.setattr( 'comitup.states.nm.get_points_ext', Mock(return_value=[pt]) ) return None @pytest.mark.parametrize( "state, action, end_state, conn, conn_list", ( ('hotspot', 'pass', 'HOTSPOT', 'hs', []), ('hotspot', 'fail', 'HOTSPOT', 'hs', []), ('hotspot', 'timeout', 'CONNECTING', 'c1', ['c1', 'c1', 'c2']), ('connecting', 'pass', 'CONNECTED', 'c1', []), ('connecting', 'fail', 'CONNECTING', 'c2', []), ('connecting', 'timeout', 'CONNECTING', 'c2', []), # note - the null connection is a test side-effect ('connected', 'pass', 'CONNECTED', '', []), ('connected', 'fail', 'HOTSPOT', 'hs', []), ('connected', 'timeout', 'HOTSPOT', 'hs', []), ) ) @pytest.mark.parametrize("thetest", ('end_state', 'conn', 'conn_list')) def test_state_transition(thetest, state, action, end_state, conn, conn_list, state_fxt, points_fxt): action_fn = states.__getattribute__(state + "_" + action) states.connection = '' if state == 'connecting': states.set_state(state.upper(), ['c1', 'c2']) else: states.set_state(state.upper()) if action == 'timeout': action_fn(states.state_id) else: action_fn() if thetest == 'end_state': assert states.com_state == end_state elif thetest == 'conn': assert states.connection == conn elif thetest == 'conn_list': assert states.conn_list == conn_list def test_state_transition_cleanup(state_fxt): states.connection = 'c1' states.set_state('CONNECTING', ['c1']) states.connecting_fail() assert states.com_state == 'HOTSPOT' def test_state_transition_no_connections(state_fxt): states.connection = 'hs' states.set_state('CONNECTING', []) # states.connecting_fail() assert states.com_state == 'HOTSPOT' @pytest.mark.parametrize("offset, match", ((-1, False), (0, True), (1, False))) def test_state_timeout_wrapper(offset, match): themock = Mock() @states.timeout def timeout_fn(): themock() assert timeout_fn(states.state_id + offset) == match assert themock.called == match def test_state_timeout_activity(): themock = Mock() @states.timeout def timeout_fn(): themock() timeout_fn(states.state_id) assert themock.called def test_state_set_hosts(): states.set_hosts('a', 'b') assert states.dns_names == ('a', 'b') @patch('comitup.states.assure_hotspot') @patch('comitup.states.nmmon.init_nmmon') def test_state_init_states(init_nmmon, assure_hs): states.init_states(('c', 'd'), []) assert states.dns_names == ('c', 'd') assert assure_hs.called assert assure_hs.call_args[0][0] == 'c' @pytest.mark.parametrize( "hostin, hostout", (('host', 'host'), ('host.local', 'host')) ) def test_state_dns_to_conn(hostin, hostout): assert states.dns_to_conn(hostin) == hostout def test_state_callback_decorator(state_globals): callback = Mock() @states.state_callback def foo_bar(): pass states.add_state_callback(callback) foo_bar() assert callback.call_args == call('FOO', 'bar') def test_state_matrix(): assert states.state_matrix('HOTSPOT').pass_fn == states.hotspot_pass @pytest.mark.parametrize("path, good", ( ("states.state_matrix('HOTSPOT').bogus_fn", False), ("states.state_matrix('HOTSPOT').bogus", False), ("states.state_matrix('HOTSPOT').pass_fn", True),)) def test_state_matrix_miss(path, good): if good: eval(path) else: with pytest.raises(AttributeError): eval(path) comitup-master-1.2.3/test/test_web.py000066400000000000000000000033501324443600300176330ustar00rootroot00000000000000 # Copyright (c) 2018 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import pytest import urllib from mock import patch, call, Mock from web import comitupweb ssid_list = [ 'simple', 'simple+', 'simplĂ©', 'simple 2', ] @pytest.fixture def app(): app = comitupweb.create_app() app.debug = True app.testing = True return app @pytest.mark.parametrize('ssid', ssid_list) def test_webapp_null(app, ssid): assert 'simpl' in ssid @pytest.mark.parametrize('ssid', ssid_list) def test_webapp_index(app, ssid, monkeypatch): point = { 'ssid': ssid, } points_mock = Mock(return_value = [point]) monkeypatch.setattr('web.comitupweb.ciu.ciu_points', points_mock) response = app.test_client().get('/') index_text = response.get_data().decode() assert ssid + "" in index_text assert "ssid=" + urllib.parse.quote(ssid) in index_text @pytest.mark.parametrize('ssid', ssid_list) def test_webapp_confirm(app, ssid, monkeypatch): quoted_ssid = urllib.parse.quote(ssid) url = "confirm?ssid={}&encrypted=encrypted".format(quoted_ssid) response = app.test_client().get(url) index_text = response.get_data().decode() assert "to " + ssid in index_text assert "value=\"" + urllib.parse.quote(ssid) in index_text @pytest.mark.parametrize('ssid', ssid_list) def test_webapp_confirm(app, ssid, monkeypatch): monkeypatch.setattr('web.comitupweb.Process', Mock()) data = { 'ssid': urllib.parse.quote(ssid), 'password': urllib.parse.quote('password'), } response = app.test_client().post('connect', data=data) index_text = response.get_data().decode() assert "to " + ssid in index_text comitup-master-1.2.3/test/test_webmgr.py000066400000000000000000000040301324443600300203350ustar00rootroot00000000000000 # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE import pytest from mock import patch, call from comitup import webmgr def test_webmgr_callback_target(): assert webmgr.callback_target() == webmgr.state_callback @pytest.fixture() def websvc_fxt(request): web_svc = webmgr.web_service def fin(): webmgr.web_service = web_svc request.addfinalizer(fin) @pytest.mark.parametrize("state, action, fn_fact, arg_fact", ( ('HOTSPOT', 'start', lambda: webmgr.stop_service, lambda: webmgr.web_service), # noqa ('HOTSPOT', 'pass', lambda: webmgr.start_service, lambda: webmgr.COMITUP_SERVICE), ('CONNECTING', 'pass', lambda: webmgr.stop_service, lambda: webmgr.COMITUP_SERVICE), ('CONNECTED', 'start', lambda: webmgr.start_service, lambda: webmgr.web_service), )) @pytest.mark.parametrize("svc", ("", "foo")) @patch('comitup.webmgr.start_service') @patch('comitup.webmgr.stop_service') def test_webmgr_callback(stop_svc, start_svc, svc, state, action, fn_fact, arg_fact, websvc_fxt): webmgr.web_service = svc webmgr.state_callback(state, action) if arg_fact(): assert fn_fact().called assert fn_fact().called_with(call(arg_fact())) else: assert not fn_fact().called if svc: assert fn_fact().called others = [(x, y) for x in ('HOTSPOT', 'CONNECTING', 'CONNECTED') for y in ('fail', 'timeout')] # noqa @pytest.mark.parametrize("state, action", [ ('CONNECTING', 'start'), ('CONNECTED', 'pass'), ] + others ) @patch('comitup.webmgr.start_service') @patch('comitup.webmgr.stop_service') def test_webmgr_no_callback(stop_svc, start_svc, state, action, websvc_fxt): webmgr.web_service = 'foo' webmgr.state_callback(state, action) assert not stop_svc.called assert not start_svc.called comitup-master-1.2.3/web/000077500000000000000000000000001324443600300152425ustar00rootroot00000000000000comitup-master-1.2.3/web/__init__.py000066400000000000000000000000001324443600300173410ustar00rootroot00000000000000comitup-master-1.2.3/web/comitupweb.conf000066400000000000000000000001031324443600300202610ustar00rootroot00000000000000[global] server.socket_port = 80 log_debug_info_filter.on = False comitup-master-1.2.3/web/comitupweb.py000077500000000000000000000037131324443600300200010ustar00rootroot00000000000000#!/usr/bin/python # Copyright (c) 2017 David Steele # # SPDX-License-Identifier: GPL-2+ # License-Filename: LICENSE # # Copyright 2016-2017 David Steele # This file is part of comitup # Available under the terms of the GNU General Public License version 2 # or later # import os import time from multiprocessing import Process import urllib import base64 from flask import Flask, render_template, request import sys sys.path.append('.') sys.path.append('..') from comitup import client as ciu # noqa def do_connect(ssid, password): time.sleep(1) ciu.ciu_connect(ssid, password) def create_app(): app = Flask(__name__) @app.route("/") def index(): points = ciu.ciu_points() for point in points: point['ssid_encoded'] = urllib.parse.quote(point['ssid']) return render_template("index.html", points=points) @app.route("/confirm") def confirm(): ssid = request.args.get("ssid", "") ssid_encoded = urllib.parse.quote(ssid.encode()) encrypted = request.args.get("encrypted", "unencrypted") return render_template( "confirm.html", ssid=ssid, encrypted=encrypted, ssid_encoded=ssid_encoded, ) @app.route("/connect", methods=['POST']) def connect(): ssid = urllib.parse.unquote(request.form["ssid"]) password = request.form["password"].encode() p = Process(target=do_connect, args=(ssid, password)) p.start() return render_template("connect.html", ssid=ssid, password=password, ) return app def main(): app = create_app() app.run(host="0.0.0.0", port=80, debug=True, threaded=True) if __name__ == '__main__': main() comitup-master-1.2.3/web/templates/000077500000000000000000000000001324443600300172405ustar00rootroot00000000000000comitup-master-1.2.3/web/templates/confirm.html000066400000000000000000000010651324443600300215650ustar00rootroot00000000000000 Comitup Confirm Selection

Comitup Confirm Selection

Press the Connect button to connect this device to {{ ssid }}.

This will terminate the current hotspot. Connect to {{ ssid }} for access.

If the connection fails, this hotspot will be re-created.

Password (if required):

comitup-master-1.2.3/web/templates/connect.html000066400000000000000000000002521324443600300215560ustar00rootroot00000000000000 Comitup Connection Attempt

Comitup Connection Attempt

Connection to {{ ssid }} is being attempted. comitup-master-1.2.3/web/templates/index.html000066400000000000000000000014741324443600300212430ustar00rootroot00000000000000 Comitup Access Point Selection

Comitup Access Point Selection

Select an access point.

    {% for point in points %}
  • {% if point.security == 'encrypted' %} {% else %} {% endif %} {{ point.ssid }} {% endfor %}
Hidden or missing SSID: