├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── app-linux ├── build.sh ├── pyproject.toml ├── requirements.txt ├── setup.cfg └── src │ └── cassowary │ ├── __init__.py │ ├── __main__.py │ ├── base │ ├── __init__.py │ ├── cfgvars.py │ ├── functions.py │ ├── helper.py │ └── log.py │ ├── cli.py │ ├── client.py │ └── gui │ ├── __init__.py │ ├── components │ ├── __init__.py │ ├── desktopitemdialog.py │ ├── main_ui.py │ ├── minidialog.py │ ├── sharesandmaps.py │ └── vmstart.py │ ├── extrares │ ├── __init__.py │ ├── cassowary.png │ ├── cassowary.svg │ ├── cassowary_app.png │ └── cassowary_app.svg │ └── qtui_files │ ├── __init__.py │ ├── desktopcreate.ui │ ├── main.ui │ ├── newmap.ui │ ├── newshare.ui │ ├── notice.ui │ └── vmstart.ui ├── app-win ├── build.bat ├── build.sh ├── requirements.txt └── src │ ├── __init__.py │ ├── base │ ├── __init__.py │ ├── cfgvars.py │ ├── command │ │ ├── __init__.py │ │ ├── cmd_apps.py │ │ ├── cmd_asso.py │ │ ├── cmd_dirs.py │ │ └── cmd_general.py │ ├── helper.py │ └── log.py │ ├── client.py │ ├── extras │ ├── app.ico │ ├── app.svg │ ├── cassowary-server.xml │ ├── cassowary_nw.vbs │ ├── hostopen.bat │ ├── nowindow.vbs │ ├── setup.bat │ └── setup.reg │ ├── package.spec │ └── server.py ├── buildall.sh └── docs ├── 1-virt-manager.md ├── 2-cassowary-install.md ├── 3-faq.md └── img ├── app-preview.gif ├── virt-manager-0.png ├── virt-manager-1.png ├── virt-manager-10.png ├── virt-manager-2.png ├── virt-manager-3.png ├── virt-manager-4.png ├── virt-manager-5.png ├── virt-manager-6.png ├── virt-manager-7.png ├── virt-manager-8.png └── virt-manager-9.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: cassowary 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: casualsnek 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A Description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **System information:** 27 | - Cassowary Linux client version: [run ```python3 -m cassowary -h``` to find version ] 28 | - Cassowary windows component version: [run ```"C:\Program Files\cassowary\cassowary.exe" -h``` to find version] 29 | - Linux Distribution: [e.g. Arch Linux] 30 | - Windows/Edition used [e.g. 10/enterprise, 7/ultimate] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | bin/ 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .idea 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo

3 |

4 | 5 | # Cassowary 6 | 7 | [![Visits Badge](https://badges.pufler.dev/visits/casualsnek/cassowary)](https://github.com/casualsnek) 8 | 9 | ![App Demo GIF](docs/img/app-preview.gif) 10 | 11 | With Cassowary you can run a Windows virtual machine and use Windows applications on Linux as if they were native applications, built upon FreeRDP and remote apps technology. 12 | 13 | **If you prefer a setup guide video instead of a wall of text, [click here.](https://www.youtube.com/watch?v=ftq-c_VgmK0)** 14 | 15 | Please give a star ⭐ or follow this project if you find it useful. 16 | 17 | **Join the discussion on Discord: [Server URL](https://discord.gg/hz4mAwSujH)** 18 | 19 | ## Cassowary supports: 20 | 21 | - Running Windows applications as if they were native applications 22 | - Opening files from a Linux host directly inside Windows applications 23 | - Using Linux apps to open files that are on a Windows VM 24 | - Allowing easy access between both the guest and host filesystems 25 | - An easy-to-use configuration utility 26 | - Creating an application launcher for Windows application 27 | - Automatically suspending the VM when no Windows application is in use and automatically resuming it when required (virt-manager only) 28 | 29 | ## This README consists of instructions for: 30 | 31 | 1. [Setting up a Windows VM with virt-manager](docs/1-virt-manager.md) 32 | 2. [Installing Cassowary on a Windows guest and Linux host](docs/2-cassowary-install.md) 33 | 3. [Extra How to's and FAQ](docs/3-faq.md) 34 | * Building Cassowary from source 35 | * How can I help? 36 | 37 |
38 | 39 | ## Building Cassowary from source 40 | 41 | This step is ONLY necessary if you don't want to use the releases from the [release page](https://github.com/casualsnek/cassowary/releases) and you want to build the `.zip` and the `.whl` files by yourself! 42 | 43 | #### Building linux application (on Linux) 44 | 45 | ```bash 46 | $ git clone https://github.com/casualsnek/cassowary 47 | $ cd cassowary/app-linux 48 | $ chmod +x build.sh 49 | $ ./build.sh 50 | ``` 51 | 52 | This will create a directory named `dist` inside `app-linux` directory containing installable `.whl` file 53 | 54 | #### Building windows application ( on Windows ) 55 | 56 | Download and install [Python3](https://python.org) (If on Windows 7 use Python 3.7) and [Git](https://git-scm.com) on the Windows system then run the commands: 57 | 58 | ```bash 59 | $ git clone https://github.com/casualsnek/cassowary 60 | $ cd cassowary\app-win 61 | $ .\build.bat 62 | ``` 63 | 64 | This will create a directory named `bin` containing the setup files. 65 | 66 | #### Building both linux and windows applications on Linux 67 | 68 | Install [wine](https://wiki.winehq.org/Download) first, in order to build Windows application on Linux. Internet access is required to download the python binary for setup. 69 | Note that Windows application built through wine may fail to run properly on some Windows systems. 70 | 71 | ```bash 72 | $ git clone https://github.com/casualsnek/cassowary 73 | $ cd cassowary 74 | $ chmod +x buildall.sh 75 | $ ./buildall.sh 76 | ``` 77 | 78 | This will create a `dist` folder inside `app-linux` which contains the installable wheel file. 79 | A `bin` folder will also be created inside `app-win` containing the setup files for Windows. 80 | 81 |
82 | 83 | ## How can I help? 84 | 85 | - Improve the README.md and/or the documentation 86 | - Report bugs or submit patches 87 | - Suggest new features or improvements on existing ones! 88 | - Support this project on [OpenCollective](https://opencollective.com/cassowary) 89 | 90 | --- 91 | 92 | ### Sponsors 93 | [![Sponsors](https://opencollective.com/cassowary/tiers/sponsor.svg?avatarHeight=36&width=600)](https://opencollective.com/cassowary) 94 | 95 | ### Backers 96 | [![Backers](https://opencollective.com/cassowary/tiers/backer.svg?avatarHeight=36&width=600)](https://opencollective.com/cassowary) 97 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /app-linux/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rm -rf ./dist 3 | rm -rf ./src/*.egg-info 4 | python3 -m build 5 | -------------------------------------------------------------------------------- /app-linux/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /app-linux/requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5 2 | libvirt-python -------------------------------------------------------------------------------- /app-linux/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = cassowary 3 | version = 0.6 4 | description = Cassowary - Integrate windows VM with linux ( client ). 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | author = Casual Snek 8 | author_email = casualsnek@pm.me 9 | url = https://github.com/causalsnek/cassowary 10 | keywords = cassowary, winapps alternative, windows vm in linux 11 | license = GPL-2.0 License 12 | 13 | [options] 14 | package_dir= 15 | =src 16 | python_requires = >= 3.0 17 | packages = find: 18 | include_package_data = True 19 | 20 | [options.entry_points] 21 | console_scripts = 22 | cassowary = cassowary:main 23 | 24 | [options.packages.find] 25 | where=src, src/base, src/gui/components, src/gui/qtui_files, src/gui/extrares 26 | 27 | [options.package_data] 28 | * = *.ui, *.png 29 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import subprocess 4 | import sys 5 | import traceback 6 | import time 7 | from .base.log import get_logger 8 | from .base.helper import path_translate_to_guest, vm_suspension_handler, full_rdp, vm_wake, fix_black_window, vm_state 9 | from .base.cfgvars import cfgvars 10 | from PyQt5.QtWidgets import QApplication 11 | from .gui.components.main_ui import MainWindow 12 | from .client import Client 13 | import threading 14 | import re 15 | 16 | 17 | 18 | def main(): 19 | logger = get_logger(__name__) 20 | cfgvars.app_root = os.path.dirname(os.path.realpath(__file__)) 21 | 22 | def start_bg_client(reconnect=True): 23 | vm_watcher = threading.Thread(target=vm_suspension_handler) 24 | vm_watcher.daemon = True 25 | vm_watcher.start() 26 | logger.info("Connecting to server....") 27 | while True: 28 | try: 29 | using_host = cfgvars.config["host"] 30 | client_ = Client(using_host, cfgvars.config["port"]) 31 | client_.init_connection() 32 | client_.accepting_forwards = True 33 | logger.info("Connected to server !") 34 | response = client_.send_wait_response(["declare-self-host"], timeout=5) 35 | if response is not False: 36 | status, data = response["status"], response["data"] 37 | if status: 38 | logger.info("Declared self as host system client to the server") 39 | # Now everything should be done by sender and receiver thread, 40 | # We just wait here to check if anything has stopped in client object, if yes, recreate client 41 | # and try again 42 | while True: 43 | if not client_.sender.is_alive() or not client_.receiver.is_alive()\ 44 | or client_.stop_connecting or using_host != cfgvars.config["host"]\ 45 | or vm_state() in [4, 5]: 46 | logger.debug("Connection seems to be lost or vm info got changed in config or vm turned off") 47 | break 48 | else: 49 | logger.info("Connected to server") 50 | time.sleep(5) 51 | else: 52 | logger.info( 53 | "Failed to declare self host.. Retrying after 5 seconds. Server response: {}, {}".format(status, 54 | data)) 55 | logger.info("Server error: {}".format(response)) 56 | client_.die() 57 | except KeyboardInterrupt: 58 | logger.info("Got keyboard interrupt.. Exiting") 59 | break 60 | except Exception as e: 61 | logger.error("Ignored exception: '%s', reconnecting to server after 5 seconds", traceback.format_exc()) 62 | time.sleep(5) 63 | if not reconnect: 64 | logger.warning("Reconnect is enabled, client is stopped") 65 | break 66 | 67 | about = """ 68 | ##################################################################################### 69 | # cassowary Client Application (Linux) - Integrate Windows VM with linux hosts # 70 | # ---------------------------- Software info ---------------------------------------# 71 | # Version : 0.6A # 72 | # GitHub : https://github.com/casualsnek/cassowary # 73 | # License : GPLv2 # 74 | # Maintainer : @casualsnek (Github) # 75 | # Email : casualsnek@pm.me # 76 | ##################################################################################### 77 | 78 | """ 79 | action_help = """ 80 | This tool itself does not do much, use 'raw-cmd' action and pass commands list as 81 | proper json While using any command put "--" after command name to pass arguments 82 | starting with dash (-) 83 | -------------------------------------------------------------------------------------------------------- 84 | Command : Description 85 | -------------------------------------------------------------------------------------------------------- 86 | guest-open : Open a file with default application on guest. 87 | Only takes one file path as parameter 88 | Usage : 89 | cassowary -c guest-open -- '/home/use/somefile.txt' 90 | ( The command is ran directly using RDP without server activity ) 91 | -------------------------------------------------------------------------------------------------------- 92 | guest-run : Runs a command on host with parameters either a application on host or guest 93 | Usage : 94 | cassowary -c guest-run -- 'C:\\..\\vlc.exe' '/home/user/test.mp4' 95 | ( Opens test.mp4 on host with vlc ) 96 | cassowary -c guest-run -- '/home/user/flashgame.exe' '-some parameters' 97 | ( Runs flashgame.exe located in windows on host with parameters) 98 | * The command is ran directly using RDP without server activity 99 | -------------------------------------------------------------------------------------------------------- 100 | raw-cmd : Sends a raw command to the windows server . Parameters is list of server commands 101 | and their parameters. (Requires at least one active RDP session) 102 | (Path translations will not be done) 103 | Usage : 104 | cassowary -c raw-cmd -- run /usr/sbin/firefox 105 | ( This sends command to server to request host to launch firefox) 106 | cassowary -c raw-cmd -- add-drive-share Y y_share 107 | ( This sends command to server share local disk Y with share name y_share) 108 | -------------------------------------------------------------------------------------------------------- 109 | path-map : Maps the given input to path on guest windows install using cached share info 110 | If it is not a valid local path input is be returned as it is 111 | Usage : 112 | cassowary -c path-map -- /home/user/document/personal.docx 113 | """ 114 | BASE_RDP_CMD = '{rdc} /d:"{domain}" /u:"{user}" /p:"{passd}" /v:{ip} +clipboard /a:drive,root,{share_root} ' \ 115 | '+decorations /cert-ignore /sound /scale:{scale} /dynamic-resolution /{mflag} {rdflag} ' \ 116 | '/wm-class:"{wmclass}" ' \ 117 | '/app:"{execu}" /app-icon:"{icon}" ' 118 | parser = argparse.ArgumentParser(description=about, formatter_class=argparse.RawDescriptionHelpFormatter) 119 | parser.add_argument('-bc', '--background-client', dest='bgc', 120 | help='Create a client which listens for host forwarded requests and replies them', 121 | action='store_true') 122 | parser.add_argument('-a', '--gui-application', dest='guiapp', 123 | help='Launch cassowary Configuration GUI', 124 | action='store_true') 125 | parser.add_argument('-f', '--full-session', dest='fullsession', help='Launches full rdp session', action='store_true') 126 | parser.add_argument('-np', '--no-polkit', dest='nopkexec', help='Prints messages in console, uses xterm with sudo' 127 | 'instead of polkit pkexec for root access', 128 | action='store_true') 129 | parser.add_argument('-wc', '--wm-class', dest='wmclass', help='Window manager class for guest-run/guest-open', 130 | default=None) 131 | parser.add_argument('-ic', '--icon', dest='icon', help='Application icon of RDP apps for Window Manager', 132 | default=None) 133 | parser.add_argument('-c', '--command', dest='command', help='The command to run (use -ch) for help') 134 | parser.add_argument('-ch', '--command-help', 135 | dest='command_help', 136 | help='Shows available commands and its description and few example usages', 137 | action='store_true') 138 | parser.add_argument('cmdline', 139 | nargs='*', 140 | help="Arguments for the used command", 141 | default=None 142 | ) 143 | args = parser.parse_args() 144 | if args.nopkexec: 145 | os.environ["DIALOG_MODE"] = "console" 146 | if len(sys.argv) == 1: 147 | parser.print_help(sys.stderr) 148 | sys.exit(1) 149 | if args.command_help: 150 | print(about + "\n" + action_help) 151 | if args.bgc: 152 | start_bg_client() 153 | if args.fullsession: 154 | vm_wake() 155 | full_rdp() 156 | sys.exit(0) 157 | if args.guiapp: 158 | logger.debug("Starting configuration GUI") 159 | app = QApplication(sys.argv) 160 | cfgvars.refresh_config() 161 | mainui = MainWindow() 162 | mainui.show() 163 | app.exec_() 164 | else: 165 | try: 166 | if not args.cmdline: 167 | print("At least one argument is required, exiting..") 168 | exit(1) 169 | else: 170 | wm_class = args.wmclass 171 | icon = args.icon 172 | multimon_enable = int(os.environ.get("RDP_MULTIMON", cfgvars.config["rdp_multimon"])) 173 | if wm_class is None: 174 | wm_class = os.environ.get("WM_CLASS", 175 | "cassowaryApp-" + args.cmdline[0].split("/")[-1].split("\\")[-1]) 176 | if icon is None: 177 | icon = os.environ.get("APP_ICON", cfgvars.config["def_icon"]) 178 | response = {"status": False} 179 | if args.command == "path-map": 180 | print(path_translate_to_guest(args.cmdline[0])) 181 | elif args.command == "guest-run": 182 | translated_paths = [path_translate_to_guest(argument) for argument in args.cmdline] 183 | # Check and translated every argument if it is a path 184 | app = QApplication(sys.argv) 185 | process = subprocess.check_output(["ps", "auxfww"]) 186 | vm_wake() 187 | if cfgvars.config["soft_launch"] and \ 188 | len(re.findall(r"freerdp.*\/wm-class:.*cassowaryApp", process.decode())) >= 1: 189 | # Use soft launch: 190 | command_and_args = "" 191 | for argument in translated_paths: 192 | if " " in argument: 193 | command_and_args = command_and_args+'"{}" '.format(argument) 194 | else: 195 | command_and_args = command_and_args + argument + " " 196 | logger.debug("Using soft launch as other RDP application is active") 197 | client__ = Client(cfgvars.config["host"], cfgvars.config["port"]) 198 | client__.init_connection() 199 | response = client__.send_wait_response( 200 | ['run-app', 'cmd.exe /c start "" {}'.format(command_and_args)], 201 | timeout=10 202 | ) 203 | logger.debug("Got soft launch response: %s", str(response)) 204 | print(response) 205 | else: 206 | cmd = BASE_RDP_CMD.format(rdflag=cfgvars.config["rdp_flags"], 207 | domain=cfgvars.config["winvm_hostname"], 208 | user=cfgvars.config["winvm_username"], 209 | passd=cfgvars.config["winvm_password"], 210 | ip=cfgvars.config["host"], scale=cfgvars.config["rdp_scale"], 211 | mflag="multimon" if multimon_enable else "span", wmclass=wm_class, 212 | rdc=cfgvars.config["app_session_client"], 213 | share_root=cfgvars.config["rdp_share_root"], 214 | execu=args.cmdline[0], icon=icon) 215 | if len(translated_paths) > 1: 216 | rd_app_args = "" 217 | for path in translated_paths[1:]: 218 | if " " in path: 219 | # This looks ugly because windows uses "" for escaping " instead of \" and this is 220 | # the only way I found so far 221 | path = '\\"\\"\\"{}\\"\\"\\"'.format(path) 222 | rd_app_args = rd_app_args + path + " " 223 | # Now problem for path with spaces is solved, but the path pointing to drive's root 224 | # | DO NOT REMOVE THIS SPACE or else path pointing to drive letter 225 | # |---| (C:| or D:| ) will not launch due to \ at end escaping the 226 | # V | ending quote 227 | cmd = cmd + '/app-cmd:"{} "'.format(rd_app_args.strip()) 228 | # cmd = cmd + " 1> /dev/null 2>&1 &" 229 | fix_black_window() 230 | logger.debug("guest-run with commandline: "+cmd) 231 | process = subprocess.Popen(["sh", "-c", "{}".format(cmd)]) 232 | process.wait() 233 | elif args.command == "guest-open": 234 | path = path_translate_to_guest(args.cmdline[0]) 235 | app = QApplication(sys.argv) 236 | process = subprocess.check_output(["ps", "auxfww"]) 237 | vm_wake() 238 | if cfgvars.config["soft_launch"] and \ 239 | len(re.findall(r"freerdp.*\/wm-class:.*cassowaryApp", process.decode())) >= 1: 240 | # Use soft launch 241 | if " " in path: 242 | path = '"{}"'.format(path) 243 | logger.debug("Using soft launch as other RDP application is active") 244 | client__ = Client(cfgvars.config["host"], cfgvars.config["port"]) 245 | client__.init_connection() 246 | response = client__.send_wait_response( 247 | ['run-app', 'cmd.exe /c start "" {} '.format(path)], 248 | timeout=10 249 | ) 250 | logger.debug("Got soft launch response: %s", str(response)) 251 | print(response) 252 | else: 253 | if " " in path: 254 | # This looks ugly because windows uses "" for escaping " instead of \" and this is the 255 | # only way I found so far 256 | path = '\\"\\"\\"{}\\"\\"\\"'.format(path) 257 | cmd = BASE_RDP_CMD.format(rdflag=cfgvars.config["rdp_flags"], 258 | domain=cfgvars.config["winvm_hostname"], 259 | user=cfgvars.config["winvm_username"], 260 | passd=cfgvars.config["winvm_password"], 261 | ip=cfgvars.config["host"], scale=cfgvars.config["rdp_scale"], 262 | mflag="multimon" if multimon_enable else "span", wmclass=wm_class, 263 | rdc=cfgvars.config["app_session_client"], 264 | share_root=cfgvars.config["rdp_share_root"], 265 | execu="cmd.exe", icon=icon) 266 | # |--- This is ugly too but without this path with spaces wont work 267 | # |----------------| 268 | # V V 269 | cmd = cmd + '/app-cmd:"/c start \\"\\"\\"\\"\\"\\" {} "'.format(path) 270 | # cmd = cmd + " 1> /dev/null 2>&1 &" 271 | fix_black_window() 272 | logger.debug("guest-open with commandline: " + cmd) 273 | process = subprocess.Popen(["sh", "-c", "{}".format(cmd)]) 274 | process.wait() 275 | elif args.command == "raw-cmd": 276 | vm_wake() 277 | client__ = Client(cfgvars.config["host"], cfgvars.config["port"]) 278 | client__.init_connection() 279 | response = client__.send_wait_response(args.cmdline, timeout=10) 280 | print(response) 281 | else: 282 | print("'{}' is not a supported command".format(args.command), "Unsupported command") 283 | except Exception as e: 284 | logger.error("Unexpected error: Exception: %s, Traceback : %s", str(e), traceback.format_exc()) 285 | sys.exit(1) 286 | sys.exit(0) 287 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/app-linux/src/cassowary/base/__init__.py -------------------------------------------------------------------------------- /app-linux/src/cassowary/base/cfgvars.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | 5 | class Vars: 6 | def __init__(self): 7 | self.app_name = "casualrdh" 8 | self.config = None 9 | self.config_dir = os.path.join(os.path.expanduser("~"), ".config", self.app_name) 10 | self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", self.app_name) 11 | self.tempdir = os.path.join("/", "tmp", self.app_name) 12 | if not os.path.exists(self.config_dir): 13 | os.makedirs(self.config_dir) 14 | if not os.path.exists(self.cache_dir): 15 | os.makedirs(self.cache_dir) 16 | if not os.path.exists(self.tempdir): 17 | os.makedirs(self.tempdir) 18 | self.base_config = { 19 | "host": "192.168.1.1", 20 | "winvm_username": "", 21 | "winvm_hostname": "", 22 | "winvm_password": "Edit It Yourself", 23 | "vm_name": "", 24 | "app_session_client": "xfreerdp", 25 | "full_session_client": "xfreerdp", 26 | "vm_auto_suspend": 0, 27 | "send_suspend_notif": 0, 28 | "libvirt_uri": "qemu:///system", 29 | "vm_suspend_delay": 600, 30 | "rdp_share_root": "/", 31 | "term": "xterm", 32 | "rdp_scale": 100, 33 | "rdp_multimon": 0, 34 | "def_icon": os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "gui/extrares/cassowary_app.png"), 35 | "rdp_flags": "", 36 | "port": 7220, 37 | "cached_drive_shares": {}, 38 | "winshare_mount_root": os.path.join("/", "mnt", self.app_name), 39 | "eom": "~~!enm!~~", 40 | "logfile": os.path.join(self.config_dir, self.app_name + ".log"), 41 | "soft_launch": 1 42 | } 43 | self.refresh_config() 44 | self.__check_config() 45 | self.shared_dict = {} 46 | 47 | def __check_config(self): 48 | # Preserve config base structure of config file 49 | changed = False 50 | for key in self.base_config: 51 | if key not in self.config: 52 | self.config[key] = self.base_config[key] 53 | changed = True 54 | if changed: 55 | self.save_config() 56 | 57 | def refresh_config(self): 58 | if not os.path.isfile(os.path.join(self.config_dir, "config.json")): 59 | with open(os.path.join(self.config_dir, "config.json"), "w+") as dmf: 60 | dmf.write(json.dumps(self.base_config)) 61 | with open(os.path.join(self.config_dir, "config.json"), "r") as dmf: 62 | self.config = json.load(dmf) 63 | 64 | def save_config(self): 65 | with open(os.path.join(self.config_dir, "config.json"), "w") as dmf: 66 | dmf.write(json.dumps(self.config)) 67 | self.refresh_config() 68 | 69 | cfgvars = Vars() 70 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/base/functions.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .cfgvars import cfgvars 3 | 4 | 5 | def get_basic_info(client, timeout=5): 6 | if client is None: 7 | return False, "Unable to fetch network mapping information ! \n Not connected to server" 8 | response = client.send_wait_response(["get-basic-info"], timeout=timeout) 9 | if response is not False: 10 | if bool(response["status"]): 11 | return True, response["data"] 12 | return False, "Request failed !\n {}".format(response["data"]) 13 | return False, "Could not fetch basic info the server due to timeout.\nMake sure server is reachable !" 14 | 15 | 16 | def get_network_maps(client, timeout=5): 17 | if client is None: 18 | return False, "Unable to fetch network mapping information ! \n Not connected to server" 19 | response = client.send_wait_response(["get-network-map"], timeout=timeout) 20 | if response is not False: 21 | if bool(response["status"]): 22 | return True, response["data"] 23 | return False, "Request failed !\n {}".format(response["data"]) 24 | return False, "Could not fetch drive maps from the server due to timeout.\nMake sure server is reachable !" 25 | 26 | 27 | def add_network_map(client, local_path, share_name, drive_letter, timeout=20): 28 | if not os.path.exists(local_path): 29 | return False, "The local path '{}' does not exist".format(local_path) 30 | if not os.path.isdir(local_path): 31 | return False, "The local path '{}' is a file, directory path is required !".format(local_path) 32 | if client is None: 33 | return False, "Unable to send new map info ! \n Not connected to server" 34 | response = client.send_wait_response(["add-network-map", local_path, share_name, drive_letter], timeout=timeout) 35 | if response is not False: 36 | if bool(response["status"]): 37 | return True, response["data"] 38 | return False, "Request failed !\n {}".format(response["data"]) 39 | return False, "Could not new send drive maps to the server due to timeout.\nMake sure server is reachable !" 40 | 41 | 42 | def rem_network_map(client, name, timeout=20): 43 | if client is None: 44 | return False, "Unable to fetch network mapping information ! \n Not connected to server" 45 | response = client.send_wait_response(["rem-network-map", name], timeout=timeout) 46 | if response is not False: 47 | if bool(response["status"]): 48 | return True, response["data"] 49 | return False, "Request failed !\n {}".format(response["data"]) 50 | return False, "Could not remove drive maps from the server due to timeout.\n Make sure server is reachable !" 51 | 52 | 53 | def get_network_shares(client, timeout=5): 54 | if client is None: 55 | return False, "Unable to fetch shared drives information ! \n Not connected to server" 56 | response = client.send_wait_response(["get-drive-shares"], timeout=timeout) 57 | if response is not False: 58 | if bool(response["status"]): 59 | cfgvars.config["cached_drive_shares"] = response["data"] 60 | cfgvars.save_config() 61 | return True, response["data"] 62 | return False, "Request failed !\n {}".format(response["data"]) 63 | return False, "Could not fetch shared drive information from the server (Timeout).\n Make sure server is reachable!" 64 | 65 | 66 | def add_network_share(client, drive_letter, share_name=None, timeout=20): 67 | drive_letter = drive_letter[0].upper() 68 | if share_name is None: 69 | share_name = drive_letter.lower() 70 | if client is None: 71 | return False, "Unable to send request to share a new drive ! \n Not connected to server" 72 | response = client.send_wait_response(["add-drive-share", drive_letter, share_name], timeout=timeout) 73 | if response is not False: 74 | if bool(response["status"]): 75 | return True, response["data"] 76 | return False, "Request failed !\n {}".format(response["data"]) 77 | return False, "Could not add new share in the server (Timeout).\n Make sure server is reachable !" 78 | 79 | 80 | def rem_network_share(client, share_name, timeout=20): 81 | if client is None: 82 | return False, "Unable to fetch network mapping information ! \n Not connected to server" 83 | response = client.send_wait_response(["rem-drive-share", share_name], timeout=timeout) 84 | if response is not False: 85 | if bool(response["status"]): 86 | return True, response["data"] 87 | return False, "Request failed !\n {}".format(response["data"]) 88 | return False, "Could not remove drive maps from the server due to timeout.\n Make sure server is reachable !" 89 | 90 | 91 | def get_installed_apps(client, timeout=20): 92 | if client is None: 93 | return False, "Unable to fetch installed application information ! \n Not connected to server" 94 | response = client.send_wait_response(["get-installed-apps"], timeout=timeout) 95 | if response is not False: 96 | if bool(response["status"]): 97 | return True, response["data"] 98 | return False, "Request failed !\n {}".format(response["data"]) 99 | return False, "Could not get installed app list from the server due to timeout.\n " \ 100 | "Make sure server is reachable or increase timeout value!" 101 | 102 | 103 | def get_association(client, timeout=20): 104 | if client is None: 105 | return False, "Unable to fetch file association list ! \n Not connected to server" 106 | response = client.send_wait_response(["get-associations"], timeout=timeout) 107 | if response is not False: 108 | if bool(response["status"]): 109 | return True, response["data"] 110 | return False, "Request failed !\n {}".format(response["data"]) 111 | return False, "Could not get file association information from the server due to timeout.\n " \ 112 | "Make sure server is reachable or increase timeout value!" 113 | 114 | 115 | def set_association(client, file_extension, timeout=20): 116 | if client is None: 117 | return False, "Unable to fetch file association information ! \n Not connected to server" 118 | response = client.send_wait_response(["set-association", file_extension], timeout=timeout) 119 | if response is not False: 120 | if bool(response["status"]): 121 | return True, response["data"] 122 | return False, "Request failed !\n {}".format(response["data"]) 123 | return False, "Could not set file association due to timeout.\n " \ 124 | "Make sure server is reachable or increase timeout value!" 125 | 126 | 127 | def unset_association(client, file_extension, timeout=20): 128 | if client is None: 129 | return False, "Unable to fetch file association information ! \n Not connected to server" 130 | response = client.send_wait_response(["unset-association", file_extension], timeout=timeout) 131 | if response is not False: 132 | if bool(response["status"]): 133 | return True, response["data"] 134 | return False, "Request failed !\n {}".format(response["data"]) 135 | return False, "Could not remove file association due to timeout.\n " \ 136 | "Make sure server is reachable or increase timeout value!" 137 | 138 | 139 | def get_exe_icon(client, file_path, timeout=20): 140 | # TODO: Actually get app icon, Return default icon if request fails !s 141 | if client is None: 142 | return False, "Unable to fetch file association information ! \n Not connected to server" 143 | response = client.send_wait_response(["get-exe-icon", file_path], timeout=timeout) 144 | if response is not False: 145 | if bool(response["status"]): 146 | return True, response["data"] 147 | return True, '' 148 | return False, "Could not get icon for '{}' .\n " \ 149 | "Make sure server is reachable or increase timeout value!".format(file_path) 150 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/base/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import RotatingFileHandler 3 | from .cfgvars import cfgvars 4 | import sys 5 | import os 6 | 7 | 8 | class DuplicateFilter(logging.Filter): 9 | def filter(self, record): 10 | # add other fields if you need more granular comparison, depends on your app 11 | current_log = (record.module, record.levelno, record.msg) 12 | if current_log != getattr(self, "last_log", None): 13 | self.last_log = current_log 14 | return True 15 | return False 16 | 17 | 18 | def get_logger(name): 19 | log_level = int(os.environ.get("LOG_LEVEL", 1)) 20 | log_levels = [logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL] 21 | logger = logging.getLogger(name) 22 | logger.setLevel(logging.NOTSET) 23 | if not logger.handlers: 24 | formatter = logging.Formatter( 25 | '[ %(asctime)s ] | [ %(levelname)6s ] : [ %(module)10s -> %(funcName)20s ] --> %(message)s ') 26 | logger.propagate = 0 27 | 28 | con_handler = logging.StreamHandler(sys.stderr) 29 | con_handler.setLevel(log_levels[log_level]) 30 | con_handler.setFormatter(formatter) 31 | logger.addHandler(con_handler) 32 | file_handler = RotatingFileHandler(cfgvars.config["logfile"], mode="a", encoding="utf-8", maxBytes=10*1024*1024) 33 | file_handler.setLevel(logging.DEBUG) 34 | file_handler.addFilter(DuplicateFilter()) 35 | file_handler.setFormatter(formatter) 36 | logger.addHandler(file_handler) 37 | logger.setLevel(log_levels[log_level]) 38 | 39 | return logger 40 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import logging 5 | import sys 6 | import traceback 7 | 8 | from .base.log import setup_logging 9 | from .base.helper import path_translate_to_guest 10 | from .base.cfgvars import cfgvars 11 | from PyQt5.QtWidgets import QApplication 12 | from .gui.components.main_ui import MainWindow 13 | 14 | if __name__ == "__main__": 15 | setup_logging() 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | about = """ 20 | ##################################################################################### 21 | # CasualRDH Client Application (Linux) - Integrate Windows VM with linux hosts # 22 | # ---------------------------- Software info ---------------------------------------# 23 | # Version : 0.1A # 24 | # GitHub : https://github.com/casualsnek/casualrdh # 25 | # License : GPLv2 # 26 | # Maintainer : @casualsnek (Github) # 27 | # Email : casualsnek@pm.me # 28 | ##################################################################################### 29 | 30 | """ 31 | action_help = """ 32 | This tool itself does not do much, use 'raw-cmd' action and pass commands list as 33 | proper json While using any command put "--" after command name to pass arguments 34 | starting with dash (-) 35 | -------------------------------------------------------------------------------------------------------- 36 | Command : Description 37 | -------------------------------------------------------------------------------------------------------- 38 | guest-open : Open a file with default application on guest. 39 | Only takes one file path as parameter 40 | Usage : 41 | casualrdh_linux -a guest-open -- '/home/use/somefile.txt' 42 | ( The command is ran directly using RDP without server activity ) 43 | -------------------------------------------------------------------------------------------------------- 44 | guest-run : Runs a command on host with parameters either a application on host or guest 45 | Usage : 46 | casualrdh_linux -a guest-run -- 'C:\\..\\vlc.exe' '/home/user/test.mp4' 47 | ( Opens test.mp4 on host with vlc ) 48 | casualrdh_linux -a guest-run -- '/home/user/flashgame.exe' '-some parameters' 49 | ( Runs flashgame.exe located in windows on host with parameters) 50 | * The command is ran directly using RDP without server activity 51 | -------------------------------------------------------------------------------------------------------- 52 | raw-cmd : Sends a raw command to the windows server . Parameters is list of server commands 53 | and their parameters. (Requires at least one active RDP session) 54 | (Path translations will not be done) 55 | Usage : 56 | casualrdh_linux -a raw-cmd -- run /usr/sbin/firefox 57 | ( This sends command to server to request host to launch firefox) 58 | casualrdh_linux -a raw-cmd -- add-drive-share Y y_share 59 | ( This sends command to server share local disk Y with share name y_share) 60 | -------------------------------------------------------------------------------------------------------- 61 | path-map : Maps the given input to path on huest windows install using cached share info 62 | If it is not a valid local path input is be returned as it is 63 | Usage : 64 | casualrdh_linux -a path-map -- /home/user/document/personal.docx 65 | """ 66 | BASE_RDP_CMD = 'xfreerdp {rdflag} /d:"{domain}" /u:"{user}" /p:"{passd}" /v:{ip} +auto-reconnect +clipboard ' \ 67 | '+home-drive -wallpaper /scale:{scale} /dynamic-resolution /{mflag} /wm-class:"{wmclass}" ' \ 68 | '/app:"{execu}" /app-icon:"{icon}"' 69 | parser = argparse.ArgumentParser(description=about, formatter_class=argparse.RawDescriptionHelpFormatter) 70 | parser.add_argument('-bc', '--background-client', dest='server', 71 | help='Create a client which listens for host forwarded requests and replies them', 72 | action='store_true') 73 | parser.add_argument('-a', '--gui-application', dest='guiapp', 74 | help='Launch casualRDH Configuration GUI', 75 | action='store_true') 76 | parser.add_argument('-np', '--no-polkit', dest='nopkexec', help='Prints messages in console, uses xterm with sudo' 77 | 'instead of polkit pkexec for root access', 78 | action='store_true') 79 | parser.add_argument('-wc', '--wm-class', dest='wmclass', help='Window manager class for guest-run/guest-open', 80 | default=None) 81 | parser.add_argument('-ic', '--icon', dest='icon', help='Application icon of RDP apps for Window Manager', 82 | default=None) 83 | parser.add_argument('-c', '--command', dest='command', help='The command to run (use -ch) for help') 84 | parser.add_argument('-ch', '--command-help', 85 | dest='command_help', 86 | help='Shows available commands and its description and few example usages', 87 | action='store_true') 88 | parser.add_argument('cmdline', 89 | nargs='*', 90 | help="Arguments for the used command", 91 | default=None 92 | ) 93 | args = parser.parse_args() 94 | if args.nopkexec: 95 | os.environ["DIALOG_MODE"] = "console" 96 | if len(sys.argv) == 1: 97 | parser.print_help(sys.stderr) 98 | exit(1) 99 | if args.command_help: 100 | print(about+"\n"+action_help) 101 | if args.server: 102 | # start_server(cfgvars.config["host"], cfgvars.config["port"]) 103 | pass 104 | if args.guiapp: 105 | logger.debug("Starting configuration GUI") 106 | app = QApplication(sys.argv) 107 | cfgvars.refresh_config() 108 | mainui = MainWindow() 109 | mainui.show() 110 | app.exec_() 111 | else: 112 | try: 113 | if not args.cmdline: 114 | print("At least one argument is required, exiting..") 115 | exit(1) 116 | else: 117 | response = {"status": False} 118 | if args.command == "path-map": 119 | print(path_translate_to_guest(args.cmdline[0])) 120 | elif args.command == "guest-run": 121 | translated_paths = [path_translate_to_guest(argument) for argument in args.cmdline] 122 | # Check and translated every argument if it is a path 123 | wm_class = args.wmclass 124 | icon = args.icon 125 | multimon_enable = int(os.environ.get("RDP_MULTIMON", cfgvars.config["rdp_multimon"])) 126 | if wm_class is None: 127 | wm_class = os.environ.get("WM_CLASS", "casualrdhApp-"+args.cmdline[0].split("/")[-1].split("\\")[-1]) 128 | if icon is None: 129 | icon = os.environ.get("APP_ICON", cfgvars.config["def_icon"]) 130 | cmd = BASE_RDP_CMD.format(rdflag=cfgvars.config["rdp_flags"], domain=cfgvars.config["winvm_hostname"], 131 | user=cfgvars.config["winvm_hostname"], passd=cfgvars.config["winvm_password"], 132 | ip=cfgvars.config["host"], scale=cfgvars.config["rdp_scale"], 133 | mflag="multimon" if multimon_enable else "span", wmclass=wm_class, 134 | execu=args.cmdline[0], icon=icon) 135 | 136 | if len(translated_paths) > 1: 137 | rd_app_args = "" 138 | for path in translated_paths[1:]: 139 | if " " in path: 140 | path = '"{}"'.format(path) 141 | rd_app_args = rd_app_args+path+" " 142 | cmd = cmd+'/app-cmd:"{}"'.format(rd_app_args.strip()) 143 | print(cmd+" 1> /dev/null 2>&1 &") 144 | elif args.command == "raw-cmd": 145 | # response = client.send_wait_response(args.cmdline, timeout=10) 146 | print(args) 147 | else: 148 | print("'{}' is not a supported command".format(args.command), "Unsupported command") 149 | except Exception as e: 150 | logger.error("Unexpected error: Exception: %s, Traceback : %s", str(e), traceback.format_exc()) 151 | print("Unexpected error.. Exiting") 152 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import json 4 | import threading 5 | import time 6 | import traceback 7 | from .base.cfgvars import cfgvars 8 | from .base.helper import create_reply, create_request, get_windows_cifs_locations, replace_vars, mount_pending, \ 9 | handle_win_ip_paths 10 | from .base.log import get_logger 11 | 12 | logger = get_logger(__name__) 13 | 14 | 15 | class Client(): 16 | def __init__(self, host="127.0.0.1", port=7220): 17 | self.send_queue = [] 18 | self.cmd_responses = {} 19 | self.stop_connecting = False 20 | self.__host = host 21 | self.__port = port 22 | self.__eom = cfgvars.config["eom"] 23 | self.server = None 24 | self.sender = None 25 | self.receiver = None 26 | 27 | self.accepting_forwards = False 28 | 29 | def init_connection(self): 30 | logger.info("Attempting to connect to server") 31 | if self.server is not None: 32 | self.server.close() 33 | 34 | # Stop threads if active 35 | if self.sender is not None: 36 | logger.debug("Sender thread seems already initialised") 37 | if self.sender.is_alive(): 38 | logger.warning("Sender thread is still alive, waiting for termination") 39 | self.stop_connecting = True 40 | self.sender.join(3) 41 | if self.receiver is not None: 42 | logger.debug("Receiver thread seems already initialised") 43 | if self.receiver.is_alive(): 44 | logger.warning("Receiver thread is still alive, waiting for termination") 45 | self.stop_connecting = True 46 | self.receiver.join(3) 47 | # Re create socket conection 48 | self.server = socket.socket() 49 | self.server.settimeout(5) 50 | self.server.connect((self.__host, self.__port)) 51 | self.server.settimeout(None) 52 | logger.info("Connected to server at {}:{}".format(self.__host, self.__port)) 53 | 54 | # Start threads 55 | logger.debug("Starting sender and receiver threads") 56 | self.__create_sub_threads() 57 | 58 | def die(self): 59 | logger.info("Attempting to stop client activity") 60 | self.stop_connecting = True 61 | self.server.close() 62 | 63 | def __receive(self): 64 | while not self.stop_connecting: 65 | message = b"" 66 | while not self.stop_connecting: 67 | try: 68 | recent_msg = self.server.recv(16000) 69 | message = message + recent_msg 70 | except Exception as e: 71 | logger.error("Error receiving messages, Exception- %s, Traceback : %s", str(e), 72 | traceback.format_exc()) 73 | self.stop_connecting = True 74 | if message.endswith(self.__eom.encode()) or message == b"": 75 | break 76 | if message == b"" or self.stop_connecting: 77 | self.stop_connecting = True 78 | self.server.close() 79 | break 80 | try: 81 | message = json.loads(message.decode("utf-8").replace(self.__eom, "")) 82 | if message["type"] == "response": 83 | message["received_on"] = int(time.time()) 84 | self.cmd_responses[message["id"]] = message 85 | elif message["type"] == "request": 86 | if self.accepting_forwards: 87 | if message["command"][0] == "xdg-open": 88 | logger.info("Received a xdg-open request") 89 | # XDG Open the requested path 90 | path = message["command"][1] 91 | handled, path = handle_win_ip_paths(path) 92 | if handled is not False: 93 | os.popen('sh -c "xdg-open \'{}\' &"'.format(path)) 94 | self.send_queue.append(create_reply(message, "ok", True)) 95 | else: 96 | self.send_queue.append(create_reply(message, 97 | "Path ({}) could not be mapped on host", 98 | False)) 99 | elif message["command"][0] == "run": 100 | print(message) 101 | command = "" 102 | errors = None 103 | for arg in message["command"][1:]: 104 | # If any argument contains space enclose it in quotes and translate any !@WINIP@! in 105 | # paths 106 | handled, path = handle_win_ip_paths(path) 107 | if handled is not False: 108 | if " " in path: 109 | path = "\'{}\'".format(path) 110 | command = command+path+" " 111 | else: 112 | errors = arg 113 | break 114 | logger.info("Received a command run request. Launching : %s", 115 | 'sh -c "{} &"'.format(command)) 116 | if not errors: 117 | os.popen('sh -c "{} &"'.format(command.strip())) 118 | self.send_queue.append(create_reply(message, "ok", True)) 119 | else: 120 | self.send_queue.append(create_reply(message, "Path ({}) could not be mapped on host".format(arg), 121 | False)) 122 | elif message["command"][0] == "open-term-at": 123 | path = message["command"][1] 124 | handled, path = handle_win_ip_paths(path) 125 | if handled is not False: 126 | print('sh -c \'{term} -e bash -c "cd \\\\"{path}\\\\""; exec $SHELL" &\''.format( 127 | path=path, 128 | term=cfgvars.config["term"] 129 | )) 130 | os.popen('{term} -e bash -c "cd \'{path}\'; exec bash" &'.format( 131 | path=path, 132 | term=cfgvars.config["term"] 133 | )) 134 | self.send_queue.append(create_reply(message, "ok", True)) 135 | else: 136 | self.send_queue.append(create_reply(message, 137 | "Path ({}) could not be mapped on host for terminal" 138 | "session", 139 | False)) 140 | else: 141 | self.request_enqueue(create_reply(message, "No handler for the command '{}'".format( 142 | command[0] 143 | ), False)) 144 | else: 145 | self.send_queue.append(create_reply( 146 | message, 147 | "No support this message type: '{}' ".format(message["type"]), 148 | False 149 | )) 150 | except (json.JSONDecodeError, KeyError, IndexError) as e: 151 | logger.error("Client received a deformed message. Message body: %s", str(message)) 152 | logger.debug("Stopping receive sub-threadd... (%s)", str(self.stop_connecting)) 153 | 154 | def __send(self): 155 | while not self.stop_connecting: 156 | for message in self.send_queue: 157 | try: 158 | self.send_queue.remove(message) 159 | logger.debug("Sending message to server") 160 | message_json = json.dumps(message) + self.__eom 161 | self.server.sendall(message_json.encode()) 162 | except Exception as e: 163 | self.send_queue.append(message) 164 | logger.error("Error receiving messages, Exception- %s, Traceback : %s", str(e), 165 | traceback.format_exc()) 166 | self.stop_connecting = True 167 | time.sleep(0.01) 168 | logger.debug("Stopping send sub-thread.d... (%s)", str(self.stop_connecting)) 169 | 170 | # These request_enqueue, get_response_of are here if user manually want to send request or get response at any time 171 | # Else send_wait_response can be used which waits for response till timeout and returns response 172 | 173 | def request_enqueue(self, command_list): 174 | message = create_request(command_list) 175 | if self.sender is not None: 176 | if self.sender.is_alive(): 177 | # Sender is alive so, add to queue 178 | self.send_queue.append(message) 179 | return message 180 | 181 | def get_response_of(self, message_id): 182 | if message_id in self.cmd_responses: 183 | response = self.cmd_responses[message_id] 184 | self.cmd_responses.pop(message_id) 185 | return response 186 | else: 187 | return False 188 | 189 | def send_wait_response(self, command_list, timeout=10): 190 | if self.receiver is not None: 191 | if self.receiver.is_alive(): 192 | message = create_request(command_list) 193 | self.send_queue.append(message) 194 | sent_at = int(time.time()) 195 | wait_till = sent_at + timeout 196 | while int(time.time()) < wait_till: 197 | response = self.get_response_of(message["id"]) 198 | if response: 199 | return response 200 | try: 201 | self.send_queue.remove(message) 202 | except ValueError: 203 | pass 204 | return False 205 | return {"status": 0, "data": "Not connected to server", "command":[]} 206 | 207 | def __create_sub_threads(self): 208 | self.stop_connecting = False 209 | self.sender = threading.Thread(target=self.__send) 210 | self.sender.daemon = True 211 | self.receiver = threading.Thread(target=self.__receive) 212 | self.receiver.daemon = True 213 | self.sender.start() 214 | self.receiver.start() 215 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/components/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/components/desktopitemdialog.py: -------------------------------------------------------------------------------- 1 | from .minidialog import MiniDialog 2 | from cassowary.base.cfgvars import cfgvars 3 | from PyQt5.QtWidgets import * 4 | from PyQt5 import uic 5 | import base64 6 | import os 7 | 8 | 9 | class DesktopItemDialog(QDialog): 10 | def __init__(self): 11 | super(DesktopItemDialog, self).__init__() 12 | uic.loadUi(os.path.join(cfgvars.app_root, "gui", "qtui_files", "desktopcreate.ui"), self) 13 | self.btn_dismiss.clicked.connect(self.close) 14 | self.dialog = MiniDialog(self) 15 | 16 | def run(self, name, description, path, version, icon=None): 17 | # Save the icon 18 | filename = "cassowary" + ''.join(e for e in name if e.isalnum()) 19 | icon_path = os.path.join(cfgvars.cache_dir, filename + ".ico") 20 | try: 21 | if not icon == "": 22 | with open(icon_path, "wb") as ico_file: 23 | ico_file.write(base64.b64decode(icon)) 24 | else: 25 | icon_path = cfgvars.config["def_icon"] 26 | except KeyError: 27 | pass 28 | self.inp_name.setText(name) 29 | self.inp_icon.setText(icon_path) 30 | self.inp_description.setText(description + " (cassowary remote application)") 31 | self.inp_comment.setText("'{}' version '{}'".format(name, version)) 32 | self.inp_command.setText("python3 -m cassowary -c guest-run -- '{}' %u".format( 33 | path.replace("\\", "\\\\").replace("'", "").replace("\"", "")) 34 | ) 35 | # Not using pixmap for now, just use css border-image 36 | self.lb_appicon.setStyleSheet("border-image: url('{}')".format(icon_path)) 37 | self.btn_save.clicked.connect(lambda: self.__save_desktop(filename)) 38 | self.exec_() 39 | 40 | def __save_desktop(self, filename): 41 | template = """[Desktop Entry] 42 | Comment={comment} 43 | Encoding=UTF-8 44 | Exec={exec_path} 45 | GenericName={generic_name} 46 | Icon={icon} 47 | Name[en_US]={name} 48 | Name={name} 49 | Categories={category} 50 | StartupWMClass={wmc} 51 | StartupNotify=true 52 | Terminal=false 53 | Type=Application 54 | Version=1.0 55 | X-KDE-RunOnDiscreteGpu=false 56 | X-KDE-SubstituteUID=false 57 | """.format(comment=self.inp_comment.text(), exec_path=self.inp_command.text(), 58 | generic_name=self.inp_description.text(), name=self.inp_name.text(), 59 | icon=self.inp_icon.text(), category=self.inp_categories.text(), 60 | wmc="cwapp-"+self.inp_name.text().replace(" ", "")) 61 | try: 62 | desktop_file_path = os.path.join(os.path.expanduser("~"), ".local", "share", "applications", 63 | filename + ".desktop") 64 | with open(desktop_file_path, "w") as df: 65 | df.write(template) 66 | os.popen("update-desktop-database {path}".format( 67 | path=os.path.join(os.path.expanduser("~"), ".local", "share", "applications") 68 | )) 69 | self.dialog.run("Desktop file created successfully !") 70 | except Exception as e: 71 | self.dialog.run("Failed to create desktop file ! \n {}".format(str(e))) 72 | self.close() 73 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/components/minidialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import uic 3 | from cassowary.base.cfgvars import cfgvars 4 | import os 5 | 6 | 7 | class MiniDialog(QDialog): 8 | def __init__(self, parent=None): 9 | super(MiniDialog, self).__init__(parent) 10 | self.path = os.path.dirname(os.path.realpath(__file__)) 11 | uic.loadUi(os.path.join(cfgvars.app_root, "gui", "qtui_files", "notice.ui"), self) 12 | self.btn_close.clicked.connect(self.close) 13 | 14 | def run(self, content): 15 | self.lb_main.setText(str(content)) 16 | self.exec_() 17 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/components/sharesandmaps.py: -------------------------------------------------------------------------------- 1 | import time 2 | from PyQt5.QtWidgets import * 3 | from PyQt5 import uic 4 | from .minidialog import MiniDialog 5 | from cassowary.base.cfgvars import cfgvars 6 | from cassowary.base.functions import add_network_share, add_network_map 7 | from cassowary.base.helper import wake_base_cmd, get_logger 8 | import os 9 | import subprocess 10 | import re 11 | 12 | logging = get_logger(__name__) 13 | 14 | 15 | class AddMapDialog(QDialog): 16 | def __init__(self): 17 | super(AddMapDialog, self).__init__() 18 | self.path = os.path.dirname(os.path.realpath(__file__)) 19 | uic.loadUi(os.path.join(cfgvars.app_root, "gui", "qtui_files", "newmap.ui"), self) 20 | self.btn_cancel.clicked.connect(self.close) 21 | self.btn_browse.clicked.connect(self.__select_dir) 22 | self.inp_localpath.textChanged.connect( 23 | lambda : self.inp_sharename.setText( 24 | self.__to_unc_equiv(os.path.abspath(self.inp_localpath.text())) 25 | ) 26 | ) 27 | 28 | def run(self, client, on_success=None): 29 | self.btn_create.clicked.connect(lambda: self.__add_clicked(client, on_success)) 30 | self.exec_() 31 | 32 | def __to_unc_equiv(self, localpath): 33 | if cfgvars.config["rdp_share_root"] == "/": 34 | return "\\\\tsclient\\root{}".format(localpath.replace("/", "\\")) 35 | else: 36 | return "\\\\tsclient\\root{}".format(localpath.replace(cfgvars.config["rdp_share_root"], "").replace("/", "\\")) 37 | 38 | def __select_dir(self): 39 | dir_path = QFileDialog.getExistingDirectory(None, 'Select a folder:', os.path.expanduser("~")) 40 | self.inp_localpath.setText(dir_path) 41 | self.inp_sharename.setText(self.__to_unc_equiv(os.path.abspath(dir_path))) 42 | 43 | def __add_clicked(self, client, on_success): 44 | dialog = MiniDialog(self) 45 | if not os.path.abspath(self.inp_localpath.text()).startswith(cfgvars.config["rdp_share_root"]): 46 | dialog.run( 47 | "Cannot map the directory '{}' !\n " 48 | "You have set your share root as '{}' and only subdirectories inside this location can be mapped".format( 49 | os.path.abspath(self.inp_localpath.text()), cfgvars.config["rdp_share_root"] 50 | )) 51 | self.close() 52 | return None 53 | if os.path.abspath(self.inp_localpath.text()) == cfgvars.config["rdp_share_root"]: 54 | dialog.run("The selected directory '' is share root and already available at Z:\\".format( 55 | cfgvars.config["rdp_share_root"]) 56 | ) 57 | self.close() 58 | return None 59 | self.inp_sharename.setText(self.__to_unc_equiv(os.path.abspath(self.inp_localpath.text()))) 60 | process = None 61 | cmd = wake_base_cmd.format(domain=cfgvars.config["winvm_hostname"], 62 | user=cfgvars.config["winvm_username"], 63 | passd=cfgvars.config["winvm_password"], 64 | ip=cfgvars.config["host"], 65 | share_root=cfgvars.config["rdp_share_root"], 66 | app="wscript.exe" 67 | )+' /app-cmd:\'{app_cmd}\''.format( 68 | app_cmd = '"C:\\Program Files\\cassowary\\nowindow.vbs" cmd /c "timeout 8"' 69 | ) 70 | proc = subprocess.check_output(["ps", "auxfww"]) 71 | if len(re.findall(r"freerdp.*\/wm-class:.*cassowaryApp", proc.decode())) < 1: 72 | logging.debug("No active RDP application, creating one before mapping drive") 73 | logging.debug("Using commandline: %s", cmd) 74 | process = subprocess.Popen(["sh", "-c", "{}".format(cmd)], stdout=subprocess.PIPE, 75 | stderr=subprocess.STDOUT) 76 | lo = False 77 | ts = int(time.time()) 78 | while process.poll() is None and not lo: 79 | for line in process.stdout: 80 | l = line.decode() 81 | print(line) 82 | if "xf_Pointer" in l: 83 | time.sleep(2) 84 | logging.debug("Application started, mapping now !") 85 | lo = True 86 | break 87 | elif int(time.time()) - ts > 10: 88 | logging.warning("Application is taking too long to start, continuing !") 89 | lo = True 90 | break 91 | logging.debug("Sending Request !") 92 | status, response = add_network_map(client, self.inp_localpath.text(), self.inp_sharename.text(), 93 | self.inp_driveletter.currentText()) 94 | logging.debug("Request complete, killing created application instance") 95 | if process is not None: 96 | process.kill() 97 | if not status: 98 | dialog.run(response) 99 | self.close() 100 | else: 101 | if on_success is not None: 102 | on_success() 103 | self.close() 104 | 105 | 106 | class AddShareDialog(QDialog): 107 | def __init__(self): 108 | super(AddShareDialog, self).__init__() 109 | uic.loadUi(os.path.join(cfgvars.app_root, "gui", "qtui_files", "newshare.ui"), self) 110 | self.btn_cancel.clicked.connect(self.close) 111 | 112 | def run(self, client, on_success=None): 113 | self.btn_createshare.clicked.connect(lambda : self.__add_clicked(client, on_success)) 114 | self.exec_() 115 | 116 | def __add_clicked(self, client, on_success): 117 | dialog = MiniDialog(self) 118 | status, response = add_network_share(client, self.inp_driveletter.currentText()) 119 | if not status: 120 | dialog.run(response) 121 | self.close() 122 | else: 123 | if on_success is not None: 124 | on_success() 125 | self.close() 126 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/components/vmstart.py: -------------------------------------------------------------------------------- 1 | import time 2 | import traceback 3 | import threading 4 | import libvirt 5 | from PyQt5.QtWidgets import * 6 | from PyQt5 import uic 7 | from cassowary.base.cfgvars import cfgvars 8 | from cassowary.base.log import get_logger 9 | import os 10 | 11 | logging = get_logger(__name__) 12 | 13 | 14 | class StartDg(QDialog): 15 | def __init__(self, parent=None): 16 | super(StartDg, self).__init__(parent) 17 | self.path = os.path.dirname(os.path.realpath(__file__)) 18 | uic.loadUi(os.path.join(cfgvars.app_root, "gui", "qtui_files", "vmstart.ui"), self) 19 | self.lb_msg.setText("The VM '{}' is not running. Do you want to start the vm now ?\n".format(cfgvars.config["vm_name"])) 20 | self.btn_startvm.clicked.connect(self.bg_st) 21 | self.btn_cancel.clicked.connect(self.close) 22 | 23 | def bg_st(self): 24 | stt = threading.Thread(target=self.wait_vm) 25 | stt.start() 26 | 27 | def wait_vm(self): 28 | self.btn_startvm.hide() 29 | self.btn_cancel.setEnabled(False) 30 | if cfgvars.config["vm_name"].strip() != "": 31 | logging.debug("Using VM") 32 | try: 33 | conn = libvirt.open(cfgvars.config["libvirt_uri"]) 34 | if conn is not None: 35 | self.lb_msg.setText(self.lb_msg.text() + "=> Connected to libvirt !\n") 36 | dom = conn.lookupByName(cfgvars.config["vm_name"]) 37 | self.lb_msg.setText(self.lb_msg.text() + "=> VM Found\n") 38 | if dom.info()[0] == 5: 39 | logging.debug("VM was found and is turned off") 40 | self.lb_msg.setText(self.lb_msg.text() + "=> Starting the vm \n") 41 | dom.create() 42 | logging.debug("Called libvirt to start the VM") 43 | self.lb_msg.setText(self.lb_msg.text() + "=> Waiting for VM networking to be active !\n") 44 | vm_ip = None 45 | logging.debug("Waiting for VM to get valid IP address\n") 46 | while vm_ip is None: 47 | interfaces = dom.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE) 48 | if interfaces is not None: 49 | for interface in interfaces: 50 | try: 51 | vm_ip = interfaces[interface]["addrs"][0]["addr"] 52 | except (KeyError, IndexError) as e: 53 | pass 54 | time.sleep(1) 55 | self.lb_msg.setText(self.lb_msg.text() + "=> Got VM IP address : "+ vm_ip) 56 | logging.debug("VM has ip '%s' now !", vm_ip) 57 | conn.close() 58 | except libvirt.libvirtError: 59 | logging.error("Cannot start VM. : %s", traceback.format_exc()) 60 | self.close() 61 | 62 | def run(self): 63 | self.exec_() 64 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/extrares/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/extrares/cassowary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/app-linux/src/cassowary/gui/extrares/cassowary.png -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/extrares/cassowary_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/app-linux/src/cassowary/gui/extrares/cassowary_app.png -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/qtui_files/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/qtui_files/desktopcreate.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DesktopDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 663 10 | 359 11 | 12 | 13 | 14 | Create Application Menu Shortcut 15 | 16 | 17 | 18 | 19 | 20 | QFrame::StyledPanel 21 | 22 | 23 | QFrame::Raised 24 | 25 | 26 | 27 | 28 | 190 29 | 20 30 | 281 31 | 39 32 | 33 | 34 | 35 | 36 | 37 | 38 | 20 39 | 30 40 | 121 41 | 17 42 | 43 | 44 | 45 | Name 46 | 47 | 48 | 49 | 50 | 51 | 20 52 | 70 53 | 131 54 | 17 55 | 56 | 57 | 58 | Description 59 | 60 | 61 | 62 | 63 | 64 | 190 65 | 60 66 | 281 67 | 39 68 | 69 | 70 | 71 | 72 | 73 | 74 | 20 75 | 110 76 | 161 77 | 21 78 | 79 | 80 | 81 | Categories (; seperated) 82 | 83 | 84 | true 85 | 86 | 87 | 88 | 89 | 90 | 190 91 | 100 92 | 281 93 | 39 94 | 95 | 96 | 97 | CasualRDH;Utility; 98 | 99 | 100 | 101 | 102 | 103 | 490 104 | 20 105 | 128 106 | 128 107 | 108 | 109 | 110 | 111 | 32 112 | 32 113 | 114 | 115 | 116 | 117 | 128 118 | 128 119 | 120 | 121 | 122 | false 123 | 124 | 125 | 126 | 127 | 128 | QFrame::Panel 129 | 130 | 131 | QFrame::Sunken 132 | 133 | 134 | 135 | 136 | 137 | Qt::AlignCenter 138 | 139 | 140 | 141 | 142 | 143 | 190 144 | 180 145 | 431 146 | 39 147 | 148 | 149 | 150 | 151 | 152 | 153 | 20 154 | 190 155 | 141 156 | 17 157 | 158 | 159 | 160 | Comment 161 | 162 | 163 | 164 | 165 | 166 | 20 167 | 230 168 | 141 169 | 17 170 | 171 | 172 | 173 | Command 174 | 175 | 176 | 177 | 178 | 179 | 190 180 | 220 181 | 431 182 | 39 183 | 184 | 185 | 186 | 187 | 188 | 189 | 20 190 | 150 191 | 161 192 | 21 193 | 194 | 195 | 196 | App icon 197 | 198 | 199 | true 200 | 201 | 202 | 203 | 204 | 205 | 190 206 | 140 207 | 281 208 | 39 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 16777215 222 | 60 223 | 224 | 225 | 226 | QFrame::StyledPanel 227 | 228 | 229 | QFrame::Raised 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | Create Menu Shortcut 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | Cancel 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/qtui_files/newmap.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 463 10 | 272 11 | 12 | 13 | 14 | Map local path to windows drive 15 | 16 | 17 | 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 451 25 | 191 26 | 27 | 28 | 29 | Share new drive from Linux to Windows 30 | 31 | 32 | 33 | 34 | 20 35 | 50 36 | 111 37 | 17 38 | 39 | 40 | 41 | Local Path 42 | 43 | 44 | 45 | 46 | 47 | 160 48 | 40 49 | 211 50 | 39 51 | 52 | 53 | 54 | 55 | 56 | 57 | 20 58 | 90 59 | 111 60 | 17 61 | 62 | 63 | 64 | Share Name 65 | 66 | 67 | 68 | 69 | 70 | 160 71 | 80 72 | 251 73 | 39 74 | 75 | 76 | 77 | 78 | 79 | 80 | true 81 | 82 | 83 | 84 | 85 | 86 | 20 87 | 130 88 | 111 89 | 17 90 | 91 | 92 | 93 | Drive Letter: 94 | 95 | 96 | 97 | 98 | 99 | 160 100 | 120 101 | 251 102 | 39 103 | 104 | 105 | 106 | 107 | D 108 | 109 | 110 | 111 | 112 | E 113 | 114 | 115 | 116 | 117 | F 118 | 119 | 120 | 121 | 122 | G 123 | 124 | 125 | 126 | 127 | H 128 | 129 | 130 | 131 | 132 | I 133 | 134 | 135 | 136 | 137 | J 138 | 139 | 140 | 141 | 142 | K 143 | 144 | 145 | 146 | 147 | L 148 | 149 | 150 | 151 | 152 | M 153 | 154 | 155 | 156 | 157 | N 158 | 159 | 160 | 161 | 162 | O 163 | 164 | 165 | 166 | 167 | P 168 | 169 | 170 | 171 | 172 | Q 173 | 174 | 175 | 176 | 177 | R 178 | 179 | 180 | 181 | 182 | S 183 | 184 | 185 | 186 | 187 | T 188 | 189 | 190 | 191 | 192 | U 193 | 194 | 195 | 196 | 197 | V 198 | 199 | 200 | 201 | 202 | W 203 | 204 | 205 | 206 | 207 | X 208 | 209 | 210 | 211 | 212 | Y 213 | 214 | 215 | 216 | 217 | 218 | 219 | 370 220 | 40 221 | 41 222 | 39 223 | 224 | 225 | 226 | : 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 16777215 236 | 70 237 | 238 | 239 | 240 | QFrame::StyledPanel 241 | 242 | 243 | QFrame::Raised 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | Create Map 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | Cancel 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/qtui_files/newshare.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 243 10 | 191 11 | 12 | 13 | 14 | Create new drive share 15 | 16 | 17 | 18 | 19 | 20 | Share new drive from windows to linux 21 | 22 | 23 | 24 | 25 | 26 | 27 | 147 28 | 16777215 29 | 30 | 31 | 32 | Drive Letter to share: 33 | 34 | 35 | Qt::AlignCenter 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | A 44 | 45 | 46 | 47 | 48 | B 49 | 50 | 51 | 52 | 53 | C 54 | 55 | 56 | 57 | 58 | D 59 | 60 | 61 | 62 | 63 | E 64 | 65 | 66 | 67 | 68 | F 69 | 70 | 71 | 72 | 73 | G 74 | 75 | 76 | 77 | 78 | H 79 | 80 | 81 | 82 | 83 | I 84 | 85 | 86 | 87 | 88 | J 89 | 90 | 91 | 92 | 93 | K 94 | 95 | 96 | 97 | 98 | L 99 | 100 | 101 | 102 | 103 | M 104 | 105 | 106 | 107 | 108 | N 109 | 110 | 111 | 112 | 113 | O 114 | 115 | 116 | 117 | 118 | P 119 | 120 | 121 | 122 | 123 | Q 124 | 125 | 126 | 127 | 128 | R 129 | 130 | 131 | 132 | 133 | S 134 | 135 | 136 | 137 | 138 | T 139 | 140 | 141 | 142 | 143 | U 144 | 145 | 146 | 147 | 148 | V 149 | 150 | 151 | 152 | 153 | W 154 | 155 | 156 | 157 | 158 | X 159 | 160 | 161 | 162 | 163 | Y 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 16777215 176 | 60 177 | 178 | 179 | 180 | QFrame::StyledPanel 181 | 182 | 183 | QFrame::Raised 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | Share 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | Cancel 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/qtui_files/notice.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 544 10 | 206 11 | 12 | 13 | 14 | Notice 15 | 16 | 17 | 18 | 19 | 20 | 21 | 11 22 | 75 23 | true 24 | 25 | 26 | 27 | Connection to guest server failed !. Make sure guest and guest server application is is running 28 | 29 | 30 | Qt::AlignCenter 31 | 32 | 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | Close 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app-linux/src/cassowary/gui/qtui_files/vmstart.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 413 10 | 242 11 | 12 | 13 | 14 | VM not running ! 15 | 16 | 17 | 18 | 19 | 20 | QFrame::StyledPanel 21 | 22 | 23 | QFrame::Raised 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 16777215 41 | 65 42 | 43 | 44 | 45 | QFrame::StyledPanel 46 | 47 | 48 | QFrame::Raised 49 | 50 | 51 | 52 | 53 | 54 | Start VM 55 | 56 | 57 | 58 | 59 | 60 | 61 | Cancel 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app-win/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd /D "%~dp0\src" 3 | rmdir /s /q ..\bin 4 | rmdir /s /q build 5 | rmdir /s /q dist 6 | echo ==^> Using pyinstaller to make executable 7 | python -m ensurepip 8 | python -m pip install pyinstaller pywin32 icoextract 9 | python -m PyInstaller package.spec --noconfirm 10 | echo ==^> Copying to setup directory 11 | mkdir ..\bin 12 | Xcopy /E /I /F /Y dist\cassowary ..\bin\cassowary 13 | Xcopy /Y /I /F /Y extras\* ..\bin\ 14 | del ..\bin\app.svg 15 | rmdir /s /q build 16 | rmdir /s /q dist 17 | cd ..\ 18 | echo ==^> Done... 19 | -------------------------------------------------------------------------------- /app-win/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Download python3 and set it up in wine environment 3 | checksum="" 4 | min_build_for=${MIN_WIN_VER:-win7} 5 | checksum_verification=${VERITY_DOWNLOADS:-0} 6 | export WINEPREFIX=/tmp/cassowary-build 7 | mkdir -p /tmp/cassowary-build 8 | download_python() 9 | { 10 | if [ ! -f /tmp/pysetup.exe ] 11 | then 12 | echo "Downloading python" 13 | if [ "$min_build_for" == "win7" ]; then 14 | echo "Keeping Windows 7 as minimum requirement" 15 | wine winecfg -v win7 16 | wget https://www.python.org/ftp/python/3.7.4/python-3.7.4-amd64.exe -O /tmp/pysetup.exe 17 | else 18 | echo "Keeping Windows 10 as minimum requirement" 19 | wine winecfg -v win10 20 | wget https://www.python.org/ftp/python/3.9.6/python-3.9.6-amd64.exe -O /tmp/pysetup.exe 21 | fi 22 | fi 23 | checksum="$(md5sum /tmp/pysetup.exe | awk '{ print $1 }')" 24 | } 25 | main() 26 | { 27 | if [ "$checksum" == "ac25cf79f710bf31601ed067ccd07deb" ] || [ "$checksum" == "531c3fc821ce0a4107b6d2c6a129be3e" ] || [ "$checksum_verification" == "0" ] ; then 28 | # Install python and run build script 29 | echo "Installing python in wine env at '/tmp/cassowary-build'. Target Min Windows version: $min_build_for" 30 | wine /tmp/pysetup.exe /quiet PrependPath=1 Include_pip=1 Include_test=0 AssociateFiles=0 Include_launcher=0 31 | wine build.bat 32 | echo "Build complete" 33 | rm /tmp/pysetup.exe 34 | else 35 | echo "Checksum of python installer ( $checksum ) do not match!" 36 | download_python 37 | main 38 | fi 39 | } 40 | download_python 41 | main 42 | 43 | -------------------------------------------------------------------------------- /app-win/requirements.txt: -------------------------------------------------------------------------------- 1 | pywin32 2 | icoextract 3 | -------------------------------------------------------------------------------- /app-win/src/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | from base.command import register_all 5 | from server import * 6 | from client import Client 7 | from base.cfgvars import cfgvars 8 | from base.helper import dialog 9 | 10 | 11 | if __name__ == "__main__": 12 | about = """ 13 | ##################################################################################### 14 | # cassowary Server/Client App (Windows) - Integrate Windows VM with linux hosts # 15 | # ---------------------------- Software info ---------------------------------------# 16 | # Version : 0.6A # 17 | # GitHub : https://github.com/casualsnek/cassowary # 18 | # License : GPLv2 # 19 | # Maintainer : @casualsnek (Github) # 20 | # Email : casualsnek@pm.me # 21 | ##################################################################################### 22 | 23 | """ 24 | action_help = """ 25 | This tool itself does not do much, use 'raw-cmd' action and pass commands list as proper json 26 | While using any command put "--" after command name to pass arguments starting with dash (-) 27 | -------------------------------------------------------------------------------------------------------- 28 | Command : Description 29 | -------------------------------------------------------------------------------------------------------- 30 | xdg-open : Open a file with 'xdg-open' on host, Used for opening file with default 31 | application for file type on host. Only takes one file path as parameter 32 | Usage : 33 | cassowary -c xdg-open -- 'C:\\Users\\Cas\\test.mp4' 34 | -------------------------------------------------------------------------------------------------------- 35 | run-cmd : Runs a command on host with parameters either a application on host or guest 36 | Usage : 37 | cassowary -c run-cmd -- '/usr/bin/mpv' 'C:\\Users\\Cas\\test.mp4' 38 | ( Opens test.mp4 on host with mpv ) 39 | cassowary -c run-cmd -- 'C:\\linuxbin.run' '-some parameters' 40 | ( Runs linuxbin.run located in windows on host with parameters) 41 | -------------------------------------------------------------------------------------------------------- 42 | raw-cmd : Sends a raw command to the server application. Parameters is list of server commands 43 | and their parameters. (Path translations will not be done) 44 | Usage : 45 | cassowary -c raw-cmd -- fwd-host run /usr/sbin/firefox 46 | ( This sends command to server to request host to launch firefox) 47 | cassowary -c raw-cmd -- add-drive-share Y y_share 48 | ( This sends command to server share local disk Y with share name y_share) 49 | """ 50 | parser = argparse.ArgumentParser(description=about, formatter_class=argparse.RawDescriptionHelpFormatter) 51 | parser.add_argument('-s', '--start-server', dest='server', help='Starts a server instance listening for connections', 52 | action='store_true') 53 | parser.add_argument('-nk', '--no-kill', dest='nokill', 54 | help='Starts a server instance listening for connections', 55 | action='store_true') 56 | parser.add_argument('-np', '--no-popup', dest='nopopup', help='Prints to console instead of showing popup on error', 57 | action='store_true') 58 | parser.add_argument('-c', '--command', dest='command', help='The command to run (use -ch) for help') 59 | parser.add_argument('-ch', '--command-help', 60 | dest='command_help', 61 | help='Shows available commands and its description and few example usages', 62 | action='store_true') 63 | parser.add_argument('cmdline', 64 | nargs='*', 65 | help="Arguments for the used command", 66 | default=None 67 | ) 68 | args = parser.parse_args() 69 | if args.nopopup: 70 | os.environ["DIALOG_MODE"] = "console" 71 | if len(sys.argv) == 1: 72 | parser.print_help(sys.stderr) 73 | sys.exit(1) 74 | register_all() 75 | if args.command_help: 76 | print(about+"\n"+action_help) 77 | if args.server: 78 | # Clear temp file directory 79 | for root, dirs, files in os.walk(cfgvars.tempdir): 80 | for name in files: 81 | os.remove(os.path.join(root, name)) 82 | while True: 83 | try: 84 | start_server(cfgvars.config["host"], cfgvars.config["port"]) 85 | sys.exit(0) 86 | except OSError as e: 87 | if "[WinError 10048]" in str(e): 88 | if not args.nokill: 89 | pid = os.popen("netstat -ano | findstr :{}".format( 90 | cfgvars.config["port"]) 91 | ).read().strip().split()[-1] 92 | os.popen("taskkill /pid {} /f".format(pid)) 93 | else: 94 | break 95 | else: 96 | break 97 | else: 98 | if not args.cmdline: 99 | print("At least one argument is required, exiting..") 100 | sys.exit(1) 101 | else: 102 | client = Client(port=cfgvars.config["port"]) 103 | client.init_connection() 104 | response = {"status":False} 105 | if args.command == "xdg-open": 106 | # Use DriveShareHelper use its path_on_host method to translate probable path strings to linux paths 107 | status, host_path = cfgvars.commands_handlers["dircommands"].path_on_host(args.cmdline[0]) 108 | message = host_path 109 | # False status means path is inaccessible from host, None means it's either 110 | # not a path (maybe URL ??) or linux path 111 | if status is not False: 112 | # Now send request to server to send request to host and forward us the host's response 113 | # TODO: Uncomment these 114 | response = client.send_wait_response(["fwd-host", "xdg-open", host_path], timeout=20) 115 | if response is not False: 116 | print(response["data"]) 117 | else: 118 | message = "Server sent no reply" 119 | if status is False or bool(response["status"]) is False: 120 | dialog( 121 | "{} (File path: {})".format(message, args.cmdline[0]), 122 | "cassowary client 'xdg-open' failed" 123 | ) 124 | elif args.command == "open-host-term": 125 | # Use DriveShareHelper use its path_on_host method to translate probable path strings to linux paths 126 | status, host_path = cfgvars.commands_handlers["dircommands"].path_on_host(args.cmdline[0]) 127 | message = host_path 128 | # False status means path is inaccessible from host, None means it's either 129 | # not a path (maybe URL ??) or linux path 130 | if status is not False: 131 | # Now send request to server to send request to host and forward us the host's response 132 | # TODO: Uncomment these 133 | response = client.send_wait_response(["fwd-host", "open-term-at", host_path], timeout=20) 134 | if response is not False: 135 | print(response["data"]) 136 | else: 137 | message = "Server sent no reply" 138 | if status is False or bool(response["status"]) is False: 139 | dialog( 140 | "{} (File path: {})".format(message, args.cmdline[0]), 141 | "cassowary client 'open-host-term' failed" 142 | ) 143 | elif args.command == "host-run": 144 | # Each argument can be a windows path, convert the path to linux path if it is a windows path 145 | # and the location presented by path exists 146 | translated_cmds = [] 147 | status = False 148 | message = "" 149 | for arg in args.cmdline: 150 | status, host_path = cfgvars.cfgvars.commands_handlers["dircommands"].path_on_host(arg) 151 | message = host_path 152 | if status is False: 153 | break 154 | translated_cmds.append(host_path) 155 | if status is not False: 156 | response = client.send_wait_response(["fwd-host", "run"] + translated_cmds, timeout=20) 157 | if response is not False: 158 | if response["status"] is True: 159 | print("The command '{}' will now be executed on host").format( 160 | " ".join(i for i in translated_cmds) 161 | ) 162 | else: 163 | message = response["data"] 164 | else: 165 | status = False 166 | message = "Server sent no reply, make sure server is active" 167 | if status is False or bool(response["status"]) is False: 168 | dialog( 169 | "{}".format(message), 170 | "cassowary client 'host-run' failed" 171 | ) 172 | elif args.command == "raw-cmd": 173 | response = client.send_wait_response(args.cmdline, timeout=10) 174 | print(response) 175 | else: 176 | dialog("'{}' is not a supported command".format(args.command), "Unsupported command") 177 | client.die() 178 | sys.exit(0) -------------------------------------------------------------------------------- /app-win/src/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/app-win/src/base/__init__.py -------------------------------------------------------------------------------- /app-win/src/base/cfgvars.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | 5 | class Vars: 6 | def __init__(self): 7 | self.app_name = "casualrdh" 8 | self.config = None 9 | self.config_dir = os.path.join(os.path.expanduser("~"), ".config", self.app_name) 10 | self.tempdir = os.path.join(os.path.expandvars("%TEMP%"), self.app_name) 11 | if not os.path.exists(self.config_dir): 12 | os.makedirs(self.config_dir) 13 | if not os.path.exists(self.tempdir): 14 | os.makedirs(self.tempdir) 15 | self.base_config = { 16 | "remembered_maps": {}, 17 | "remembered_assocs": {}, 18 | "port": 7220, 19 | "eom": "~~!enm!~~", 20 | "logfile": os.path.join(self.config_dir, self.app_name + ".log"), 21 | "host": "0.0.0.0", 22 | "assoc_ftype": "casualhXDGO", 23 | "xdg_open_handle": "wscript.exe \"C:\\Program Files\\cassowary\\cassowary_nw.vbs\" -c xdg-open -- \"%1\"", 24 | } 25 | self.refresh_config() 26 | self.__check_config() 27 | self.cmd_queue_host_only = [] 28 | self.cmd_host_only_responses = {} 29 | self.cmd_host_only_ids = [] 30 | self.commands = {} 31 | self.commands_handlers = {} 32 | self.shared_dict = {} 33 | 34 | def register_cmd(self, runner_class): 35 | obj = runner_class() 36 | if obj.NAME not in self.commands_handlers: 37 | self.commands_handlers[obj.NAME] = obj 38 | else: 39 | print("Conflicting name '{}', Used by: {} AND {}".format( 40 | obj.NAME, 41 | self.commands_handlers[obj.NAME].DESC, 42 | runner_class.DESC 43 | )) 44 | exit(1) 45 | for command in obj.CMDS: 46 | if command not in self.commands: 47 | self.commands[command] = obj.NAME 48 | else: 49 | print("Command: '{}' already associated to-> {} : {}".format( 50 | command, 51 | self.commands_handlers[self.commands[command]].DESC, 52 | obj.DESC 53 | )) 54 | exit(1) 55 | return True 56 | 57 | def __check_config(self): 58 | # Preserve config base structure of config file 59 | changed = False 60 | for key in self.base_config: 61 | if key not in self.config: 62 | self.config[key] = self.base_config[key] 63 | changed = True 64 | if changed: 65 | self.save_config() 66 | 67 | def refresh_config(self): 68 | if not os.path.isfile(os.path.join(self.config_dir, "config.json")): 69 | with open(os.path.join(self.config_dir, "config.json"), "w+") as dmf: 70 | dmf.write(json.dumps(self.base_config)) 71 | with open(os.path.join(self.config_dir, "config.json"), "r") as dmf: 72 | self.config = json.load(dmf) 73 | 74 | def save_config(self): 75 | 76 | with open(os.path.join(self.config_dir, "config.json"), "w") as dmf: 77 | dmf.write(json.dumps(self.config)) 78 | self.refresh_config() 79 | 80 | 81 | cfgvars = Vars() 82 | -------------------------------------------------------------------------------- /app-win/src/base/command/__init__.py: -------------------------------------------------------------------------------- 1 | from .cmd_dirs import DriveShareHelper 2 | from .cmd_apps import ApplicationData 3 | from .cmd_asso import FileAssociation 4 | from .cmd_general import CmdGeneral 5 | 6 | from ..helper import cfgvars 7 | 8 | 9 | def register_all(): 10 | cfgvars.register_cmd(DriveShareHelper) 11 | cfgvars.register_cmd(ApplicationData) 12 | cfgvars.register_cmd(FileAssociation) 13 | cfgvars.register_cmd(CmdGeneral) 14 | return True 15 | -------------------------------------------------------------------------------- /app-win/src/base/command/cmd_apps.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import subprocess 4 | import glob 5 | import traceback 6 | import pywintypes 7 | import winreg 8 | import win32api 9 | from base64 import b64encode 10 | from icoextract import IconExtractor 11 | from base.log import get_logger 12 | 13 | 14 | logger = get_logger(__name__) 15 | 16 | 17 | class ApplicationData: 18 | def __init__(self): 19 | self.CMDS = ["get-installed-apps", "get-exe-icon"] 20 | self.NAME = "installedappcommands" 21 | self.DESC = "Provides installed app information from registry including app icons" 22 | 23 | @staticmethod 24 | def __get_exe_info(path_to_exe): 25 | logger.debug("Getting application descriptions for: '{}' ".format(path_to_exe)) 26 | try: 27 | language, codepage = win32api.GetFileVersionInfo(path_to_exe, '\\VarFileInfo\\Translation')[0] 28 | stringFileInfo = u'\\StringFileInfo\\%04X%04X\\%s' % (language, codepage, "FileDescription") 29 | stringVersion = u'\\StringFileInfo\\%04X%04X\\%s' % (language, codepage, "FileVersion") 30 | 31 | description = win32api.GetFileVersionInfo(path_to_exe, stringFileInfo) 32 | version = win32api.GetFileVersionInfo(path_to_exe, stringVersion) 33 | except: 34 | description = "unknown" 35 | version = "unknown" 36 | logger.warning("Failed to get version information for '%s' : %s", path_to_exe, traceback.format_exc()) 37 | return [description, version] 38 | 39 | @staticmethod 40 | def __get_exe_descr(path_to_exe): 41 | logger.debug("Getting application descriptions for: '{}' ".format(path_to_exe)) 42 | try: 43 | language, codepage = win32api.GetFileVersionInfo(path_to_exe, '\\VarFileInfo\\Translation')[0] 44 | stringFileInfo = u'\\StringFileInfo\\%04X%04X\\%s' % (language, codepage, "FileDescription") 45 | 46 | description = win32api.GetFileVersionInfo(path_to_exe, stringFileInfo) 47 | description = ' '.join(description.split('.')) 48 | except: 49 | description = "unknown" 50 | return description 51 | 52 | @staticmethod 53 | def __get_exe_image(exe_path): 54 | img_str = "" 55 | try: 56 | extractor = IconExtractor(exe_path) 57 | icon = extractor.get_icon() 58 | img_str = b64encode(icon.getvalue()).decode() 59 | except: 60 | img_str = "" 61 | return img_str 62 | 63 | @staticmethod 64 | def __find_installed(): 65 | logger.debug("Getting list of installed applications") 66 | applications = [] 67 | for installation_mode in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]: 68 | # Open HKLM and HKCU and look for installed applications 69 | try: 70 | registry = winreg.ConnectRegistry(None, installation_mode) 71 | app_path_key = winreg.OpenKey(registry, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths") 72 | except FileNotFoundError: 73 | # If the Key does not exist instead of throwing an exception safely ignore it 74 | pass 75 | # Open the directories on App Path -Accessing requires a defined integer, we loop Until we get WinError 259 76 | for n in range(1000): 77 | try: 78 | app_entry = winreg.OpenKey(app_path_key, winreg.EnumKey(app_path_key, n)) 79 | path_key = winreg.QueryValueEx(app_entry, None) 80 | path = os.path.expandvars(str(path_key[0])) 81 | if path not in applications: 82 | applications.append(path) 83 | except Exception as e: 84 | if "[WinError 259]" in str(e): 85 | break 86 | elif "[WinError 2]" in str(e): 87 | pass 88 | else: 89 | logger.error("Exception while scanning for apps ! : "+traceback.format_exc()) 90 | pass 91 | return applications 92 | 93 | @staticmethod 94 | def __find_installed_with_info(): 95 | logger.debug("Getting list of installed applications") 96 | applications = [] 97 | ps_command = "Get-AppxPackage | Select {:s}" 98 | fields = ["Name", "Version", "InstallLocation"] 99 | applications_dict = {field: [] for field in fields} 100 | for field in fields: 101 | command = ["powershell.exe", ps_command.format(field)] 102 | p = subprocess.Popen(command, stdout=subprocess.PIPE) 103 | output, errors = p.communicate() 104 | if not errors: 105 | for line in output.decode("utf-8").splitlines(): 106 | line = line.strip() 107 | if line and (not "---" in line) and (not field in line): 108 | applications_dict[field].append(line) 109 | else: 110 | logger.error("Exception while scanning for apps ! : "+traceback.format_exc()) 111 | return applications 112 | 113 | for i, path in enumerate(applications_dict["InstallLocation"]): 114 | executables = glob.glob(path + "\\**\\*.exe", recursive=True) 115 | n = len(executables) 116 | if n == 0: 117 | executable = None 118 | elif n > 1: 119 | executable = None 120 | executable_size = 0 121 | for file in executables: 122 | size = os.path.getsize(file) 123 | if size > executable_size: 124 | executable = file 125 | executable_size = size 126 | else: 127 | executable = executables[0] 128 | 129 | if executable is not None and not "SystemApps" in executable: 130 | if applications_dict["Name"][i] is None: 131 | name = os.basename(executable).split('.')[0] 132 | name = name[0].upper() + name[1:] 133 | else: 134 | name = ' '.join(applications_dict["Name"][i].split('.')) 135 | applications.append([name, executable, applications_dict["Version"][i]]) 136 | 137 | return applications 138 | 139 | def run_cmd(self, cmd): 140 | if cmd[0] == "get-installed-apps": 141 | installed_apps = [] 142 | # app_name: [path_to_exe, version, icon ] 143 | if sys.getwindowsversion().major < 10: 144 | apps = self.__find_installed() 145 | for app in apps: 146 | app_info = self.__get_exe_info(app) 147 | # NOTE: app_info[0] is app icon remove it from the returned data 148 | installed_apps.append([app_info[0], app, app_info[1]]) 149 | # [description, path, version] 150 | else: 151 | apps_with_info = self.__find_installed_with_info() 152 | for app_with_info in apps_with_info: 153 | app_descr = self.__get_exe_descr(app_with_info[1]) 154 | if app_descr is not None and app_descr != "unknown": 155 | app_with_info[0] = app_descr 156 | installed_apps.append(app_with_info) 157 | # [description, path, version] 158 | return True, installed_apps 159 | elif cmd[0] == "get-exe-icon": 160 | return True, self.__get_exe_image(cmd[1]) 161 | else: 162 | return False, None 163 | -------------------------------------------------------------------------------- /app-win/src/base/command/cmd_asso.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import winreg 3 | from ..helper import uac_cmd_exec 4 | from ..cfgvars import cfgvars 5 | from base.log import get_logger 6 | 7 | logger = get_logger(__name__) 8 | 9 | 10 | class FileAssociation: 11 | def __init__(self): 12 | # 13 | # config.json -> [remembered_assocs] 14 | # : 15 | # "zip" : "CompressedFolder" 16 | 17 | self.CMDS = ["get-associations", "set-association", "unset-association"] 18 | self.NAME = "assocommands" 19 | self.DESC = "File Associations Helper - Creates 'casualhXDGO' ftype which xdg-opens a file on host system" 20 | # This should create a ftype which is associated to xdg_open_handle bin 21 | # Setting new associations should record previous ftype of extension for easy removal of association 22 | # Check if ftype is set to xdg_open_handle_bin %1 if not set it 23 | cmd_out = uac_cmd_exec("ftype {}".format(cfgvars.config["assoc_ftype"]), noadmin=True) 24 | if cfgvars.config["xdg_open_handle"] not in cmd_out: 25 | logger.error( 26 | "Ftype not associated to app, dobule clicking file will not trigger xdg-open, Expected %s - Got: %s", 27 | cfgvars.config["xdg_open_handle"], 28 | cmd_out.strip()) 29 | logger.debug("Trying to fix ftype and open command string") 30 | # ftype not set 31 | if not cmd_out.startswith(cfgvars.config["assoc_ftype"]): 32 | uac_cmd_exec("assoc .xdgo={ftype}".format(ftype=cfgvars.config["assoc_ftype"])) 33 | uac_cmd_exec('ftype {ftype}={launch_str}'.format( 34 | ftype=cfgvars.config["assoc_ftype"], 35 | launch_str=cfgvars.config["xdg_open_handle"]) 36 | ) 37 | 38 | @staticmethod 39 | def __get_associations(): 40 | logger.debug("Fetching file extensions associated to our ftype") 41 | associated_exts = [] 42 | cmd_out = uac_cmd_exec("assoc", noadmin=True, non_blocking=False) 43 | if cmd_out is None: 44 | return False, "Cannot get association info" 45 | else: 46 | cmd_out = cmd_out.split("\n") 47 | for line in cmd_out: 48 | line = line.strip() 49 | if line.endswith(cfgvars.config["assoc_ftype"]): 50 | associated_exts.append(line.split("=")[0][1:]) 51 | return True, associated_exts 52 | 53 | @staticmethod 54 | def __set_association(file_format): 55 | file_format = file_format.strip() 56 | logger.debug("Setting association to file format '.%s'", file_format) 57 | # Get current associated ftype: assoc .format -> .format=ftype 58 | cmd_out = uac_cmd_exec("assoc .{extension}".format(extension=file_format), noadmin=True) 59 | old_association = "" 60 | if file_format in ["exe", "msi"]: 61 | return False, "Refusing to change association for this file type as this might break the system ! " 62 | if cmd_out is not None: 63 | if " association not found for extension" not in cmd_out: 64 | # A ftype is associated with this extension 65 | old_association = cmd_out.strip().split("=")[1] 66 | cmd_out = uac_cmd_exec("assoc .{extension}={ftype}".format(extension=file_format, 67 | ftype=cfgvars.config["assoc_ftype"] 68 | )) 69 | if cmd_out is not None: 70 | if cfgvars.config["assoc_ftype"] in cmd_out: 71 | # Remove persistent handler 72 | try: 73 | registry = winreg.ConnectRegistry(None, winreg.HKEY_CLASSES_ROOT) 74 | ph = winreg.OpenKey(registry, r".{extension}".format(extension=file_format)) 75 | winreg.DeleteKey(ph, "PersistentHandler") 76 | registry.Close() 77 | logger.debug("Persistent handler entry removed !") 78 | except (FileNotFoundError, OSError): 79 | logger.warning("No persistent handler for file extension '.%s' -> ERR: %s", 80 | file_format, traceback.format_exc()) 81 | # Create a value in capability with the file extension 82 | try: 83 | registry = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 84 | cap = winreg.CreateKey(registry, r"SOFTWARE\casualhXDGO\Capablities\FileAssociations") 85 | winreg.SetValueEx(cap, '.{ext}'.format(ext=file_format), 0, winreg.REG_SZ, 86 | cfgvars.config["assoc_ftype"]) 87 | registry.Close() 88 | logger.debug("Capability added ") 89 | except (FileNotFoundError, OSError): 90 | logger.error("Could not associate with extension properly -> %s", traceback.format_exc()) 91 | # Do not remove old ftype if it already exists, else add new entry 92 | cfgvars.refresh_config() 93 | if file_format not in cfgvars.config["remembered_assocs"]: 94 | # Backup for this file extension does not exist, create one 95 | cfgvars.config["remembered_assocs"][file_format] = old_association 96 | cfgvars.save_config() 97 | return True, "Successfully associated to file extension '.{}'".format(file_format) 98 | else: 99 | logger.error("Unexpected error while setting association. '%s' ", cmd_out) 100 | return False, "Unexpected error while setting association. '{}' ".format(cmd_out) 101 | else: 102 | logger.error("Failed to set association for '.%s', Maybe the UAC prompt was dismissed !", file_format) 103 | return False, "Failed to set new association, Maybe the UAC prompt was dismissed !" 104 | else: 105 | logger.error("Command failed for getting current associations") 106 | return False, "Error getting current association for '{}' extension. (Command failed) ".format(file_format) 107 | 108 | @staticmethod 109 | def __unset_associations(file_format): 110 | file_format = file_format.strip() 111 | logger.debug("Removing association to file format '.%s'", file_format) 112 | cmd_out = uac_cmd_exec("assoc .{extension}".format(extension=file_format), noadmin=True) 113 | if cmd_out is not None: 114 | current_ftype = cmd_out.strip().split("=")[1] 115 | cfgvars.refresh_config() 116 | if current_ftype == cfgvars.config["assoc_ftype"]: 117 | # This file extension is associated to this tool, rollback to older ftype from backup 118 | # If backup does not exist for this file extension, remove the association (Unknown file format) 119 | old_ftype = "unknown" 120 | if file_format in cfgvars.config["remembered_assocs"]: 121 | old_ftype = cfgvars.config["remembered_assocs"][file_format] if cfgvars.config["remembered_assocs"][ 122 | file_format].strip() != "" else "unknown" 123 | cmd_out = uac_cmd_exec("assoc .{extension}={ftype}".format(extension=file_format, ftype=old_ftype)) 124 | if cmd_out is not None: 125 | if old_ftype in cmd_out: 126 | # Un setting was successful 127 | try: 128 | registry = winreg.ConnectRegistry(None, winreg.HKEY_CLASSES_ROOT) 129 | ph = winreg.OpenKey(registry, 130 | r".\{extension}".format(extension=file_format)) 131 | winreg.DeleteKey(ph, "PersistentHandler") 132 | logger.debug("Persistent handler removed") 133 | except (FileNotFoundError, OSError): 134 | logger.debug("No persistent handler for file extension '.%s'", file_format) 135 | # Remove value in capability with the file extension 136 | try: 137 | registry = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 138 | cap = winreg.OpenKey(registry, 139 | r"SOFTWARE\casualhXDGO\Capablities\FileAssociations") 140 | winreg.DeleteValue(cap, ".{ext}".format(ext=file_format)) 141 | registry.Close() 142 | logger.debug("Capablity removed ") 143 | except (FileNotFoundError, OSError): 144 | logger.error("Could not remove association with extension properly -> %s", 145 | traceback.format_exc()) 146 | if file_format in cfgvars.config["remembered_assocs"]: 147 | cfgvars.config["remembered_assocs"].pop(file_format) 148 | cfgvars.save_config() 149 | return True, "Removed association with file format '.{}'".format(file_format) 150 | else: 151 | logger.error("Unexpected error while removing association. '%s' ", cmd_out) 152 | return False, "Unexpected error while un setting association. '{}' ".format(cmd_out) 153 | else: 154 | logger.error("Failed to remove association, Maybe the UAC prompt was dismissed !") 155 | return False, "Failed to unset association, Maybe the UAC prompt was dismissed !" 156 | elif file_format in cfgvars.config["remembered_assocs"]: 157 | # Association was previously set but was changed manually, just remove from backup, return false 158 | cfgvars.config["remembered_assocs"].pop(file_format) 159 | cfgvars.save_config() 160 | logger.warning("No longer associated to this format '.%s', maybe external change was done", file_format) 161 | return False, "No longer associated to this file format '{}', (Backup cleared)".format(file_format) 162 | else: 163 | logger.warning("Not associated to this file format '.%s'", file_format) 164 | return False, "Not associated to this file format '{}'".format(file_format) 165 | else: 166 | logger.error("Command failed for getting current associations") 167 | return False, "Error getting current association for '{}' extension. (Command failed) ".format(file_format) 168 | 169 | def run_cmd(self, cmd): 170 | if cmd[0] == "get-associations": 171 | logger.debug("Got assocation data request") 172 | status, message = self.__get_associations() 173 | return status, message 174 | elif cmd[0] == "set-association": 175 | logger.debug("Got set association requets. Params: %s", str(cmd)) 176 | try: 177 | status, message = self.__set_association(cmd[1]) 178 | return status, message 179 | except IndexError: 180 | logger.warning("Error setting file association, Command - %s : %s", str(cmd), traceback.format_exc()) 181 | return False, "Command 'set-association' is missing parameter. Required: file_extension " 182 | elif cmd[0] == "unset-association": 183 | logger.debug("Got unset association requets. Params: %s", str(cmd)) 184 | try: 185 | status, message = self.__unset_associations(cmd[1]) 186 | return status, message 187 | except IndexError: 188 | logger.warning("Error Unsetting file association, Command - %s : %s", str(cmd), traceback.format_exc()) 189 | return False, "Command 'unset-association' is missing parameter. Required: file_extension " 190 | else: 191 | return False, None 192 | -------------------------------------------------------------------------------- /app-win/src/base/command/cmd_dirs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from ..helper import uac_cmd_exec 4 | from ..cfgvars import cfgvars 5 | from base.log import get_logger 6 | 7 | 8 | logger = get_logger(__name__) 9 | 10 | 11 | class DriveShareHelper: 12 | def __init__(self): 13 | self.CMDS = ["add-drive-share", "get-drive-shares", "rem-drive-share", "add-network-map", "get-network-map", 14 | "rem-network-map"] 15 | self.NAME = "dircommands" 16 | self.DESC = "Share Windows drives to network to access them from linux" 17 | # Check if Z is shared 18 | if os.path.exists("\\\\tsclient\\root"): 19 | active_maps = self.__get_active_network_maps() 20 | if "Z:\\" not in active_maps: 21 | logger.debug("Z: is not mapped... Mapping now") 22 | # No map for root, create one now 23 | uac_cmd_exec("net use Z: /delete", noadmin=True, timeout=8) 24 | cmd_out = uac_cmd_exec("net use Z: \"\\\\tsclient\\root\" /persistent:Yes", noadmin=True, timeout=8) 25 | if "command completed successfully" in cmd_out: 26 | logger.debug("Host root is now mounted as Z :) ") 27 | else: 28 | logger.error("Failed to map host root to Z: -> net use command returned: "+cmd_out) 29 | else: 30 | logger.debug("Looks like this is not a RDP session, share for root folder not found") 31 | 32 | @staticmethod 33 | def __get_active_network_maps(): 34 | logger.debug("Getting active network location mapping using net use") 35 | cmd_out = uac_cmd_exec("net use", noadmin=True) 36 | if cmd_out is None: 37 | logger.error("Failed to get output of net share, command error") 38 | return False, "Failed to get output of command 'net share' " 39 | else: 40 | cmd_out = cmd_out.split("\n") 41 | i = 6 42 | active_maps = {} 43 | nl = [y for y in cmd_out[3].split(" ") if y != ""] 44 | position_local = cmd_out[3].find(nl[1]) 45 | position_remote = cmd_out[3].find(nl[2]) 46 | position_network = cmd_out[3].find(nl[3]) 47 | while i < len(cmd_out) - 3: 48 | status = cmd_out[i][0:position_local].strip() 49 | local = cmd_out[i][position_local:position_remote].strip() 50 | remote = cmd_out[i][position_remote:position_network].strip() 51 | active_maps[local.upper() + "\\"] = [remote, status] 52 | i = i + 1 53 | return active_maps 54 | 55 | def __add_new_map(self, remote_path, network_location, drive_letter): 56 | drive_letter = drive_letter[0].upper() 57 | logger.debug("Attempting to add mapping from '%s' to letter '%s' resolving to host path '%s'", 58 | network_location, drive_letter, remote_path) 59 | active_maps = self.__get_active_network_maps() 60 | if drive_letter == "Z": 61 | return False, "Drive letter 'Z' is reserved for root '/' " 62 | if network_location.endswith("\\"): 63 | network_location = network_location[:-1] 64 | if len(remote_path) > 1 and remote_path.endswith("/"): 65 | remote_path = remote_path[:-1] 66 | if not drive_letter + ":\\" in active_maps: 67 | # Drive letter is not used, good to go, 68 | command_line = "net use {drive_letter}: \"{network_location}\" /persistent:Yes".format( 69 | drive_letter=drive_letter, 70 | network_location=network_location.strip() 71 | ) 72 | cmd_out = uac_cmd_exec(command_line, noadmin=True) 73 | active_maps = self.__get_active_network_maps() 74 | if drive_letter + ":\\" in active_maps: 75 | cfgvars.refresh_config() 76 | # Keep a record of remote host path for this map 77 | cfgvars.config["remembered_maps"][drive_letter + ":\\"] = [network_location, remote_path] 78 | cfgvars.save_config() 79 | logger.debug("Added mapping from '%s' to letter '%s' resolving to host path '%s'", network_location, 80 | drive_letter, remote_path) 81 | return True, "Host path '{}' is now mapped to drive '{}' ".format(remote_path, drive_letter) 82 | else: 83 | logger.error("Unknown error while trying to map to drive '%s' => Used commandline: %s", cmd_out, 84 | command_line) 85 | return False, "Error while trying to map to drive: \n'{}'".format(cmd_out.split("\n")[-1]) 86 | else: 87 | logger.warning("Map not added, drive letter already in use") 88 | return False, "The drive letter is already in use" 89 | 90 | def __remove_map(self, name): 91 | logger.debug("Attempting to remove network mapping of : %s", name) 92 | if not name.startswith("\\\\"): 93 | name = name[0].upper() + ":" 94 | cmd_out = uac_cmd_exec("net use {name} /delete".format(name=name), noadmin=True) 95 | active_maps = self.__get_active_network_maps() 96 | if name+"\\" not in active_maps: 97 | # Remove from remembered maps if it is there 98 | logger.debug("Trying to remove location'%s' from remembered map", name) 99 | if not name.startswith("\\\\"): 100 | if name + "\\" in cfgvars.config["remembered_maps"]: 101 | # Remove it 102 | cfgvars.config["remembered_maps"].pop(name + "\\") 103 | logger.debug("Removed mapping") 104 | else: 105 | for letter in cfgvars.config["remembered_maps"]: 106 | if cfgvars.config["remembered_maps"][letter][0] == name: 107 | # Remove it 108 | logger.debug("Removed mapping") 109 | cfgvars.config["remembered_maps"].pop(letter) 110 | # Save changes 111 | cfgvars.save_config() 112 | logger.debug("Network location Map to '%s' drive removed", name) 113 | return True, "Network location Map to '{}' drive removed".format(name) 114 | else: 115 | logger.warning("Not removed network location Map to '%s' does not exist or error : %s -> MAPS: %s", 116 | name, cmd_out, str(active_maps)) 117 | return False, "Error removing mapping for '{}'. \n".format(name, cmd_out.split("\n")[-1]) 118 | 119 | @staticmethod 120 | def __get_shared_drives(no_hostname=False): 121 | # This should return the current network share location of drives 122 | # : 123 | # 124 | logger.debug("Getting shared local drives using net share") 125 | cmd_out = uac_cmd_exec("net share", noadmin=True) 126 | if cmd_out is None: 127 | logger.error("Failed to get shared local drive data, command error") 128 | return False, "Failed to get output of command 'net share' " 129 | else: 130 | cmd_out = cmd_out.split("\n") 131 | i = 4 132 | nl = [y for y in cmd_out[1].split(" ") if y != ""] 133 | position_resource = cmd_out[1].find(nl[1]) 134 | position_remark = cmd_out[1].find(nl[2]) 135 | shared_drives = {} 136 | while i < len(cmd_out) - 3: 137 | share_name = cmd_out[i][0:position_resource].strip() 138 | if not share_name.endswith("$"): 139 | resource = cmd_out[i][position_resource:position_remark].strip() 140 | pc_name = os.environ['COMPUTERNAME'] 141 | if no_hostname: 142 | pc_name = "!@WINSHAREIP@!" 143 | shared_drives[resource] = ["\\\\{}\\{}".format(pc_name, share_name), share_name] 144 | i = i + 1 145 | return True, shared_drives 146 | 147 | def __add_new_share(self, drive_letter, share_name=None): 148 | # Check if share name is already used, if not create a share else return false 149 | share_name = share_name.replace(" ", "") 150 | drive_letter = drive_letter[0].upper() 151 | logger.debug("Attempting to create a new share for: %s, using name: %s", drive_letter, share_name) 152 | status, active_shares = self.__get_shared_drives() 153 | if share_name is None: 154 | share_name = drive_letter 155 | # The drive is ready to be shared 156 | if drive_letter+":\\" in active_shares: 157 | logger.warning("Drive %s semms to shared already ! Shared drives: %s ", drive_letter, str(active_shares)) 158 | return "Drive %s:\\ is already shared !" 159 | cmd_out = uac_cmd_exec("net share {sharename}={location} /grant:everyone,FULL".format( 160 | sharename=share_name, location=drive_letter + ":\\")) 161 | status, active_shares = self.__get_shared_drives() 162 | if cmd_out is None or cmd_out == "": 163 | logger.error("Failed to create new share, Maybe the UAC prompt was dismissed !") 164 | return False, "Failed to create new share, Maybe the UAC prompt was dismissed !" 165 | elif drive_letter+":\\" in active_shares or cmd_out.split(" ")[0] == share_name: 166 | logger.debug("Share created for drive letter '%s' at '%s"'', drive_letter, 167 | "\\\\{}\\{}".format(os.environ['COMPUTERNAME'], share_name)) 168 | return True, {drive_letter + ":\\": "\\\\{}\\{}".format(os.environ['COMPUTERNAME'], share_name)} 169 | else: 170 | logger.error("Error while creating share. `%s` -> Active shares: %s", cmd_out, str(active_shares)) 171 | return False, "Error while creating share. \n'{}'".format(cmd_out) 172 | 173 | def __remove_share(self, name): 174 | # Check if share exists and if it exists remove it 175 | logger.debug("Attempting to delete a share: %s", name) 176 | cmd_out = uac_cmd_exec("net share {name} /delete".format(name=name)) 177 | status, active_shares = self.__get_shared_drives() 178 | shared_switched = {} 179 | print("Active shares: "+str(active_shares)) 180 | if status is None: 181 | logger.error("Failed to fetch currently shared drives") 182 | return False, active_shares 183 | for drive_letter in active_shares: 184 | shared_switched[active_shares[drive_letter][1]] = [active_shares[drive_letter][0], drive_letter] 185 | if cmd_out is None: 186 | logger.error("Failed to delete the share, Maybe the UAC prompt was dismissed !") 187 | return False, "Failed to delete the share, Maybe the UAC prompt was dismissed !" 188 | elif name not in shared_switched: 189 | logger.debug("Shared '%s' was removed", name) 190 | return True, "Share '{}' was deleted !".format(name) 191 | else: 192 | logger.error("Error while removing share.. `%s`", cmd_out) 193 | return False, "Error while removing share. \n'{}'".format(cmd_out.strip()) 194 | 195 | def path_on_host(self, local_path): 196 | full_path = local_path 197 | 198 | logger.debug("Attempting path translation for '%s'", full_path) 199 | # If this a a existing path, return translated path, else just return None and input path untouched 200 | # Linux like path like "/home/" will not exist here and will be returned back as it is 201 | if os.path.exists(full_path): 202 | full_path = os.path.abspath(local_path) 203 | # This regular expression to test UNC paths, found on internet 204 | np_reg = r'^(\\\\[\w\.\-_]+\\[^\\/:<>|""]+)((?:\\[^\\/:<>|""]+)*\\?)$' 205 | cfgvars.refresh_config() 206 | 207 | # This file is from linux host 208 | if full_path.startswith("Z:\\") or full_path.startswith("\\\\tsclient\\root\\"): 209 | # Just remove prefix and replace slashes and we are good to go 210 | return True, full_path.replace("Z:", "").replace("\\\\tsclient\\root", "").replace("\\", "/") 211 | 212 | # The initials of full_path 'C:\\' is in keys of remembered_maps, It is a path on linux mapped to drive 213 | elif full_path[:3] in cfgvars.config["remembered_maps"]: 214 | # Just replace "drive_letter":\\ with the path on host and "\" to "/" 215 | host_path = full_path.replace(full_path[:2], 216 | cfgvars.config["remembered_maps"][full_path[:3]][1] 217 | ).replace("\\", "/") 218 | return True, host_path 219 | 220 | # Check if it looks like a network path, if it is checked, it is network location of shared location on 221 | # linux 222 | elif re.match(np_reg, full_path): 223 | for remembered_map in cfgvars.config["remembered_maps"]: 224 | if full_path.startswith(cfgvars.config["remembered_maps"][remembered_map][0]): 225 | host_path = full_path.replace( 226 | cfgvars.config["remembered_maps"][remembered_map][0], 227 | cfgvars.config["remembered_maps"][remembered_map][1] 228 | ).replace("\\", "/") 229 | return True, host_path 230 | logger.warning("This path (network location) should be mapped and shared for path translation", full_path) 231 | return False, "This network location should be mapped and shared before it can be accessed from host" 232 | 233 | # Now it must be any other local drive on windows, check if it is shared, if not return error 234 | else: 235 | status, shares = self.__get_shared_drives() 236 | if full_path[:3] in shares: 237 | # as: C:\dir\somefile.pdf -> \\PC-HOSTNAME\sharename\dir\somefile.pdf -> 238 | # !@WINSHAREIP@!/cdrive/dir/somefile.pdf 239 | net_path = shares[full_path[:3]][0] 240 | 241 | host_path = full_path.replace(full_path[:2], net_path).replace( 242 | "\\\\{}\\".format(os.environ['COMPUTERNAME']), 243 | "\\\\!@WINSHAREIP@!\\" 244 | ).replace("\\", "/") 245 | return True, host_path 246 | else: 247 | # The drive is not shared, return false 248 | logger.error("This path cannot be translated to path on host because it is not shared (%s)", full_path) 249 | return False, "This file's path cannot be translated to path on host because drive containing it " \ 250 | "is not shared " 251 | else: 252 | # This is not a path that exists on Windows, maybe any other string, maybe linux path (Don't deal with 253 | # it here) 254 | logger.warning("The path '%s' is not a valid windows path or path does not exist", full_path) 255 | return None, local_path 256 | 257 | def run_cmd(self, cmd): 258 | # Passed cmd is a list containing command followed by parameters, This function get called 259 | # When a command is received normally 260 | if cmd[0] == "add-drive-share": 261 | try: 262 | status, message = self.__add_new_share(cmd[1], cmd[2]) 263 | return status, message 264 | except IndexError: 265 | logger.warning("Failed creating drive share. Params: %s", str(cmd)) 266 | return False, "Command 'add-drive-share' is missing parameter. Required: drive_letter, share_name " 267 | elif cmd[0] == "get-drive-shares": 268 | status, message = self.__get_shared_drives(no_hostname=True) 269 | return status, message 270 | elif cmd[0] == "rem-drive-share": 271 | try: 272 | status, message = self.__remove_share(cmd[1]) 273 | return status, message 274 | except IndexError: 275 | logger.warning("Failed removing drive share. Params: %s", str(cmd)) 276 | return False, "Command 'rem-drive-share' is missing parameter. Required: share_name/drive_letter " 277 | elif cmd[0] == "add-network-map": 278 | try: 279 | status, message = self.__add_new_map(cmd[1], cmd[2], cmd[3]) 280 | return status, message 281 | except IndexError: 282 | logger.warning("Failed adding network map. Params: %s", str(cmd)) 283 | return False, "Command 'add-network-map' missing parameter. Required: host_path, network_location, " \ 284 | "drive_letter " 285 | elif cmd[0] == "get-network-map": 286 | cfgvars.refresh_config() 287 | return True, cfgvars.config["remembered_maps"] 288 | elif cmd[0] == "rem-network-map": 289 | try: 290 | status, message = self.__remove_map(cmd[1]) 291 | return status, message 292 | except IndexError: 293 | logger.warning("Failed removing network map. Params: %s", str(cmd)) 294 | return False, "Command 'rem-network-map' is missing parameter. Required: network_location/drive_letter" 295 | else: 296 | return False, None 297 | -------------------------------------------------------------------------------- /app-win/src/base/command/cmd_general.py: -------------------------------------------------------------------------------- 1 | import os 2 | from base.log import get_logger 3 | import sys 4 | 5 | logger = get_logger(__name__) 6 | 7 | 8 | class CmdGeneral: 9 | def __init__(self): 10 | self.CMDS = ["get-basic-info", "close-server", "close-sessions", "run-app"] 11 | self.NAME = "generalcommands" 12 | self.DESC = "Provides simple commands to get information/run applications" 13 | 14 | @staticmethod 15 | def __get_names(): 16 | return True, {"username": os.environ["USERNAME"], "hostname": os.environ["COMPUTERNAME"]} 17 | 18 | def run_cmd(self, cmd): 19 | if cmd[0] == "get-basic-info": 20 | status, data = self.__get_names() 21 | return True, data 22 | elif cmd[0] == "close-server": 23 | sys.exit(0) 24 | elif cmd[0] == "close-sessions": 25 | os.popen("logout") 26 | return False, "Error" 27 | elif cmd[0] == "run-app": 28 | command_str = '' 29 | for i in range(1, len(cmd)): 30 | if " " in cmd[i]: 31 | command_str = command_str+'"{}" '.format(cmd[i]) 32 | else: 33 | command_str = command_str+cmd[i]+' ' 34 | os.system(command_str) 35 | return True, "App with commandline executed: '{}' ".format(command_str) 36 | else: 37 | return False, None 38 | -------------------------------------------------------------------------------- /app-win/src/base/helper.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import os 4 | import time 5 | from .cfgvars import cfgvars 6 | 7 | 8 | def dialog(body, title=""): 9 | if os.environ.get("DIALOG_MODE") != "console": 10 | script = 'x=msgbox("{}" ,0, "{}")'.format(body, title) 11 | temp_file = os.path.join(cfgvars.tempdir, str(random.randint(11111, 999999)) + ".vbs") 12 | if not os.path.exists(cfgvars.tempdir): 13 | os.makedirs(cfgvars.tempdir) 14 | with open(temp_file, "w") as tmpf: 15 | tmpf.write(script) 16 | os.system('wscript.exe ' + temp_file) 17 | try: 18 | os.remove(temp_file) 19 | except OSError: 20 | pass 21 | else: 22 | print(body) 23 | 24 | 25 | def randomstr(leng=4): 26 | return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(leng)) 27 | 28 | 29 | def create_request(command, message_id=None): 30 | if type(command) is not list: 31 | if type(command) is dict: 32 | command = list(command) 33 | command = str(command).split(" ") 34 | if message_id is None: 35 | message_id = randomstr() 36 | return { 37 | "id": message_id, 38 | "type": "request", 39 | "command": command 40 | } 41 | 42 | 43 | def create_reply(message, data, status): 44 | message["type"] = "response" 45 | message["status"] = 1 if status else 0 46 | message["data"] = data 47 | return message 48 | 49 | def randomstr(leng=4): 50 | return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(leng)) 51 | 52 | 53 | def uac_cmd_exec(command, timeout=3, noadmin=False, non_blocking=True): 54 | temp_file = os.path.join(cfgvars.tempdir, randomstr(8)+ ".vbs") 55 | script = ''' 56 | CreateObject("Shell.Application").ShellExecute "cmd.exe", "/c {command}> {temp_file}.out 2>&1", "", "runas", 1 57 | '''.format(command=command.replace('"', '""'), temp_file=temp_file.replace('"', '""')) 58 | if not os.path.exists(cfgvars.tempdir): 59 | os.makedirs(cfgvars.tempdir) 60 | if not noadmin: 61 | with open(temp_file, "w") as tmpf: 62 | tmpf.write(script) 63 | os.system('wscript.exe ' + temp_file) 64 | else: 65 | if non_blocking: 66 | os.system("cmd /c {command}> {temp_file}.out 2>&1".format(command=command, temp_file=temp_file)) 67 | else: 68 | return os.popen(command).read().strip() 69 | command_exec_at = int(time.time()) 70 | output = None 71 | while int(time.time()) <= command_exec_at + timeout: 72 | time.sleep(0.5) 73 | if os.path.isfile(temp_file + ".out"): 74 | with open(temp_file + ".out", "r") as tmpf: 75 | output = tmpf.read() 76 | if output.strip() != "": 77 | break 78 | try: 79 | os.remove(temp_file + ".out") 80 | os.remove(temp_file) 81 | except OSError: 82 | pass 83 | return output 84 | -------------------------------------------------------------------------------- /app-win/src/base/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import RotatingFileHandler 3 | from .cfgvars import cfgvars 4 | import sys 5 | import os 6 | 7 | 8 | class DuplicateFilter(logging.Filter): 9 | def filter(self, record): 10 | # add other fields if you need more granular comparison, depends on your app 11 | current_log = (record.module, record.levelno, record.msg) 12 | if current_log != getattr(self, "last_log", None): 13 | self.last_log = current_log 14 | return True 15 | return False 16 | 17 | 18 | def get_logger(name): 19 | log_level = int(os.environ.get("LOG_LEVEL", 1)) 20 | log_levels = [logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL] 21 | logger = logging.getLogger(name) 22 | logger.setLevel(logging.NOTSET) 23 | if not logger.handlers: 24 | formatter = logging.Formatter( 25 | '[ %(asctime)s ] | [ %(levelname)6s ] : [ %(module)10s -> %(funcName)20s ] --> %(message)s ') 26 | logger.propagate = 0 27 | 28 | con_handler = logging.StreamHandler(sys.stderr) 29 | con_handler.setLevel(log_levels[log_level]) 30 | con_handler.setFormatter(formatter) 31 | logger.addHandler(con_handler) 32 | file_handler = RotatingFileHandler(cfgvars.config["logfile"], mode="a", encoding="utf-8", maxBytes=10*1024*1024) 33 | file_handler.setLevel(logging.DEBUG) 34 | file_handler.addFilter(DuplicateFilter()) 35 | file_handler.setFormatter(formatter) 36 | logger.addHandler(file_handler) 37 | logger.setLevel(log_levels[log_level]) 38 | 39 | return logger 40 | -------------------------------------------------------------------------------- /app-win/src/client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | import threading 4 | import time 5 | import traceback 6 | from base.log import get_logger 7 | from base.cfgvars import cfgvars 8 | from base.helper import create_reply, create_request 9 | 10 | 11 | logger = get_logger(__name__) 12 | 13 | 14 | class Client: 15 | def __init__(self, host="127.0.0.1", port=7220): 16 | self.send_queue = [] 17 | self.cmd_responses = {} 18 | self.stop_connecting = False 19 | self.__eom = cfgvars.config["eom"] 20 | self.__host = host 21 | self.__port = port 22 | self.server = None 23 | self.sender = None 24 | self.receiver = None 25 | 26 | def die(self): 27 | logger.info("Attempting to stop client activity") 28 | self.stop_connecting = True 29 | if self.server is not None: 30 | self.server.close() 31 | 32 | def init_connection(self): 33 | logger.info("Attempting to connect to server") 34 | if self.server is not None: 35 | self.server.close() 36 | 37 | # Stop threads if active 38 | if self.sender is not None: 39 | logger.debug("Sender thread seems already initialised") 40 | if self.sender.is_alive(): 41 | logger.warning("Sender thread is still alive, waiting for termination") 42 | self.stop_connecting = True 43 | self.sender.join(3) 44 | if self.receiver is not None: 45 | logger.debug("Receiver thread seems already initialised") 46 | if self.receiver.is_alive(): 47 | logger.warning("Receiver thread is still alive, waiting for termination") 48 | self.stop_connecting = True 49 | self.receiver.join(3) 50 | # Re create socket connection 51 | logger.debug("Creating new connection") 52 | self.server = socket.socket() 53 | self.server.settimeout(5) 54 | print(self.__host, self.__port) 55 | self.server.connect(("127.0.0.1", self.__port)) 56 | self.server.settimeout(None) 57 | logger.info("Connected to server at {}:{}".format(self.__host, self.__port)) 58 | 59 | # Start threads 60 | logger.debug("Starting sender and receiver threads") 61 | self.__create_sub_threads() 62 | 63 | def __receive(self): 64 | logger.debug("Receiver up") 65 | while not self.stop_connecting: 66 | message = b"" 67 | while not self.stop_connecting: 68 | try: 69 | recent_msg = self.server.recv(10000000) 70 | message = message + recent_msg 71 | except Exception as e: 72 | logger.error("Error receiving messages, Exception- %s, Traceback : %s", str(e), 73 | traceback.format_exc()) 74 | self.stop_connecting = True 75 | break 76 | if message.endswith(self.__eom.encode()) or message == b"": 77 | break 78 | if message == b"" or self.stop_connecting: 79 | self.stop_connecting = True 80 | self.server.close() 81 | break 82 | try: 83 | message = json.loads(message.decode("utf-8").replace(self.__eom, "")) 84 | if message["type"] == "response": 85 | message["received_on"] = int(time.time()) 86 | self.cmd_responses[message["id"]] = message 87 | else: 88 | self.send_queue.append(create_reply( 89 | message, 90 | "Windows client does not support this message type: '{}' ".format(message["type"]), 91 | False 92 | )) 93 | except (json.JSONDecodeError, KeyError, IndexError) as e: 94 | logger.error("Client received a deformed message. Message body: %s", str(message)) 95 | logger.debug("Stopping receive sub-thread... (%s)", str(self.stop_connecting)) 96 | 97 | def __send(self): 98 | logger.debug("Sender up") 99 | while not self.stop_connecting: 100 | for message in self.send_queue: 101 | try: 102 | message_json = json.dumps(message) + self.__eom 103 | self.server.sendall(message_json.encode()) 104 | self.send_queue.remove(message) 105 | except Exception as e: 106 | logger.error("Error receiving messages, Exception- %s, Traceback : %s", str(e), 107 | traceback.format_exc()) 108 | self.stop_connecting = True 109 | time.sleep(0.01) 110 | logger.debug("Stopping send sub-thread... (%s)", str(self.stop_connecting)) 111 | 112 | # These request_enqueue, get_response_of are here if user manually want to send request or get response at any time 113 | # Else send_wait_response can be used which waits for response till timeout and returns response 114 | 115 | def request_enqueue(self, command_list): 116 | message = create_request(command_list) 117 | if self.sender is not None: 118 | if self.sender.is_alive(): 119 | # Sender is alive so, add to queue 120 | self.send_queue.append(message) 121 | return message 122 | 123 | def get_response_of(self, message_id): 124 | if message_id in self.cmd_responses: 125 | response = self.cmd_responses[message_id] 126 | self.cmd_responses.pop(message_id) 127 | return response 128 | else: 129 | return False 130 | 131 | def send_wait_response(self, command_list, timeout=10): 132 | if self.receiver is not None: 133 | if self.receiver.is_alive(): 134 | message = create_request(command_list) 135 | self.send_queue.append(message) 136 | sent_at = int(time.time()) 137 | wait_till = sent_at + timeout 138 | while int(time.time()) < wait_till: 139 | response = self.get_response_of(message["id"]) 140 | if response: 141 | return response 142 | try: 143 | self.send_queue.remove(message) 144 | except ValueError: 145 | logger.error("Value error while waiting for response. Traceback : %s", traceback.format_exc()) 146 | pass 147 | return False 148 | return {"status": 0, "data": "Not connected to server", "command": []} 149 | 150 | def __create_sub_threads(self): 151 | self.stop_connecting = False 152 | self.sender = threading.Thread(target=self.__send) 153 | self.sender.daemon = True 154 | self.receiver = threading.Thread(target=self.__receive) 155 | self.receiver.daemon = True 156 | self.sender.start() 157 | self.receiver.start() 158 | -------------------------------------------------------------------------------- /app-win/src/extras/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/app-win/src/extras/app.ico -------------------------------------------------------------------------------- /app-win/src/extras/cassowary-server.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/app-win/src/extras/cassowary-server.xml -------------------------------------------------------------------------------- /app-win/src/extras/cassowary_nw.vbs: -------------------------------------------------------------------------------- 1 | If WScript.Arguments.Count = 0 Then 2 | Wscript.Echo "NoConsole: At least one argument is required" 3 | WScript.Quit 4 | End If 5 | ' Join every arguments 6 | command_line = """C:\Program Files\cassowary\cassowary.exe""" 7 | For i = 0 to (WScript.Arguments.Count - 1) 8 | arg = WScript.Arguments(i) 9 | If InStr(WScript.Arguments(i), " ") > 0 Then 10 | ' String contains space quote it 11 | ' If string contains quote double it (Escape it) 12 | 13 | If InStr(WScript.Arguments(i), """") > 0 Then 14 | arg = Replace(arg,"""","""""") 15 | Wscript.Echo arg & ": Contains quote" 16 | End If 17 | 18 | ' Now quote it as it contains space 19 | arg = """" & arg & """" 20 | 21 | End If 22 | command_line = command_line & " " & arg 23 | 24 | Next 25 | Set objShell = WScript.CreateObject("WScript.Shell") 26 | objShell.Run Trim(command_line), 0, True -------------------------------------------------------------------------------- /app-win/src/extras/hostopen.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | start wscript.exe "C:\Program Files\cassowary\cassowary_nw.vbs" -c xdg-open -- %1 3 | -------------------------------------------------------------------------------- /app-win/src/extras/nowindow.vbs: -------------------------------------------------------------------------------- 1 | If WScript.Arguments.Count = 0 Then 2 | Wscript.Echo "NoConsole: At least one argument is required" 3 | WScript.Quit 4 | End If 5 | ' Join every arguments 6 | command_line = "" 7 | For i = 0 to (WScript.Arguments.Count - 1) 8 | arg = WScript.Arguments(i) 9 | If InStr(WScript.Arguments(i), " ") > 0 Then 10 | ' String contains space quote it 11 | ' If string contains quote double it (Escape it) 12 | 13 | If InStr(WScript.Arguments(i), """") > 0 Then 14 | arg = Replace(arg,"""","""""") 15 | Wscript.Echo arg & ": Contains quote" 16 | End If 17 | 18 | ' Now quote it as it contains space 19 | arg = """" & arg & """" 20 | 21 | End If 22 | command_line = command_line & " " & arg 23 | 24 | Next 25 | Set objShell = WScript.CreateObject("WScript.Shell") 26 | objShell.Run Trim(command_line), 0, True -------------------------------------------------------------------------------- /app-win/src/extras/setup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: Copied from the link below 3 | :::::::::::::::::::::::::::::::::::::::::::: 4 | :: Elevate.cmd - Version 4 5 | :: Automatically check & get admin rights 6 | :: see "https://stackoverflow.com/a/12264592/1016343" for description 7 | :::::::::::::::::::::::::::::::::::::::::::: 8 | @echo off 9 | CLS 10 | ECHO. 11 | ECHO ============================= 12 | ECHO Running Admin shell 13 | ECHO ============================= 14 | 15 | :init 16 | setlocal DisableDelayedExpansion 17 | set cmdInvoke=1 18 | set winSysFolder=System32 19 | set "batchPath=%~0" 20 | for %%k in (%0) do set batchName=%%~nk 21 | set "vbsGetPrivileges=%temp%\OEgetPriv_%batchName%.vbs" 22 | setlocal EnableDelayedExpansion 23 | 24 | :checkPrivileges 25 | NET FILE 1>NUL 2>NUL 26 | if '%errorlevel%' == '0' ( goto gotPrivileges ) else ( goto getPrivileges ) 27 | 28 | :getPrivileges 29 | if '%1'=='ELEV' (echo ELEV & shift /1 & goto gotPrivileges) 30 | ECHO. 31 | ECHO ************************************** 32 | ECHO Invoking UAC for Privilege Escalation 33 | ECHO ************************************** 34 | 35 | ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%" 36 | ECHO args = "ELEV " >> "%vbsGetPrivileges%" 37 | ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%" 38 | ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%" 39 | ECHO Next >> "%vbsGetPrivileges%" 40 | 41 | if '%cmdInvoke%'=='1' goto InvokeCmd 42 | 43 | ECHO UAC.ShellExecute "!batchPath!", args, "", "runas", 1 >> "%vbsGetPrivileges%" 44 | goto ExecElevation 45 | 46 | :InvokeCmd 47 | ECHO args = "/c """ + "!batchPath!" + """ " + args >> "%vbsGetPrivileges%" 48 | ECHO UAC.ShellExecute "%SystemRoot%\%winSysFolder%\cmd.exe", args, "", "runas", 1 >> "%vbsGetPrivileges%" 49 | 50 | :ExecElevation 51 | "%SystemRoot%\%winSysFolder%\WScript.exe" "%vbsGetPrivileges%" %* 52 | exit /B 53 | 54 | :gotPrivileges 55 | setlocal & cd /d %~dp0 56 | if '%1'=='ELEV' (del "%vbsGetPrivileges%" 1>nul 2>nul & shift /1) 57 | 58 | :::::::::::::::::::::::::::: 59 | ::START 60 | :::::::::::::::::::::::::::: 61 | REM Run shell as admin (example) - put here code as you like 62 | echo "==> Killing running cassowary instance" 63 | taskkill /im cassowary.exe /f 64 | echo "==> Copying files to Program Files directory" 65 | Xcopy /E /I /Y cassowary "C:\Program Files\cassowary" 66 | echo "==> Copying no console script and hostopen.bat" 67 | Xcopy /I /Y cassowary_nw.vbs "C:\Program Files\cassowary\" 68 | Xcopy /I /Y nowindow.vbs "C:\Program Files\cassowary\" 69 | Xcopy /I /Y hostopen.bat "C:\Program Files\cassowary\" 70 | Xcopy /I /Y app.ico "C:\Program Files\cassowary\" 71 | echo "==> Importing registry keys" 72 | reg import setup.reg 73 | echo "==> Setting up path variables" 74 | SETX /M PATH "%PATH%;C:\Program Files\cassowary\" 75 | echo "==> Creating scheduled task to run server after logon" 76 | schtasks /Create /XML cassowary-server.xml /tn cassowary-server /f 77 | echo "==> Allowing cassowary and RDP connection through firewall" 78 | netsh advfirewall firewall add rule name="Cassowary Server" dir=in action=allow program="C:\Program Files\cassowary\cassowary.exe" enable=yes 79 | netsh advfirewall firewall set rule group="remote desktop" new enable=Yes 80 | echo " ==> Setup complete, press any key to exit .... Restart for all changes to take place !" 81 | pause 82 | -------------------------------------------------------------------------------- /app-win/src/extras/setup.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem] 3 | "LongPathsEnabled"=dword:00000001 4 | 5 | [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System] 6 | "EnableLUA"=dword:00000000 7 | 8 | [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server] 9 | "fDenyTSConnections"=dword:00000000 10 | 11 | [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server] 12 | "fSingleSessionPerUser"=dword:00000000 13 | 14 | [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server] 15 | "fDenyTSConnections"=dword:00000000 16 | 17 | [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server] 18 | "fSingleSessionPerUser"=dword:00000000 19 | 20 | [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\TSAppAllowList] 21 | "fDisabledAllowList"=dword:00000001 22 | 23 | [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services] 24 | "fAllowUnlistedRemotePrograms"=dword:00000001 25 | 26 | [HKEY_CLASSES_ROOT\Directory\Background\shell\Open host terminal here\command] 27 | @="wscript.exe \"C:\\Program Files\\cassowary\\cassowary_nw.vbs\" -c open-host-term -- \"%V\"" 28 | 29 | [HKEY_CLASSES_ROOT\*\shell\Open with default app on host\command] 30 | @="wscript.exe \"C:\\Program Files\\cassowary\\cassowary_nw.vbs\" -c xdg-open -- \"%1\"" 31 | 32 | [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\cassowary.exe] 33 | @="C:\\Program Files\\cassowary\\cassowary.exe" 34 | UseUrl=dword:00000001 35 | 36 | [HKEY_LOCAL_MACHINE\SOFTWARE\casualhXDGO] 37 | "exe64"="wscript.exe \"C:\\Program Files\\cassowary\\cassowary_nw.vbs\" -c xdg-open --" 38 | 39 | [HKEY_LOCAL_MACHINE\SOFTWARE\casualhXDGO\Capablities] 40 | 41 | [HKEY_LOCAL_MACHINE\SOFTWARE\casualhXDGO\Capablities\FileAssociations] 42 | ".xdgo"="casualhXDGO" 43 | 44 | [HKEY_CLASSES_ROOT\casualhXDGO] 45 | @="Cassowary Remote Launch File" 46 | 47 | [HKEY_CLASSES_ROOT\casualhXDGO\DefaultIcon] 48 | @="C:\\Program Files\\cassowary\\app.ico" 49 | 50 | [HKEY_CLASSES_ROOT\casualhXDGO\Shell] 51 | 52 | [HKEY_CLASSES_ROOT\casualhXDGO\Shell\Open] 53 | 54 | [HKEY_CLASSES_ROOT\casualhXDGO\Shell\Open\Command] 55 | @="wscript.exe \"C:\\Program Files\\cassowary\\cassowary_nw.vbs\" -c xdg-open -- \"%1\"" 56 | 57 | -------------------------------------------------------------------------------- /app-win/src/package.spec: -------------------------------------------------------------------------------- 1 | consold_a = Analysis(['__init__.py'], 2 | pathex=['.'], 3 | binaries=[], 4 | datas=[], 5 | hiddenimports=[], 6 | hookspath=[], 7 | runtime_hooks=[], 8 | excludes=[], 9 | win_no_prefer_redirects=False, 10 | win_private_assemblies=False, 11 | noarchive=False) 12 | 13 | 14 | consold_pyz = PYZ(consold_a.pure, consold_a.zipped_data) 15 | 16 | consold_exe = EXE(consold_pyz, 17 | consold_a.scripts, 18 | [], 19 | exclude_binaries=True, 20 | name='cassowary', 21 | debug=False, 22 | bootloader_ignore_signals=False, 23 | strip=False, 24 | upx=True, 25 | console=True ) 26 | 27 | noconsold_coll = COLLECT(consold_exe, 28 | consold_a.binaries, 29 | consold_a.zipfiles, 30 | consold_a.datas, 31 | strip=False, 32 | upx=True, 33 | name='cassowary') 34 | -------------------------------------------------------------------------------- /app-win/src/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | from base.cfgvars import cfgvars 4 | from base.log import get_logger 5 | from base.helper import create_reply, randomstr 6 | import threading 7 | import socket 8 | import json 9 | import traceback 10 | 11 | logger = get_logger(__name__) 12 | 13 | 14 | class ClientConnectionThread(): 15 | def __init__(self, name, session, address): 16 | self.name = name 17 | logger.debug("[ClientID: %s] New client connected", self.name) 18 | self.session = session # Socket connection to client 19 | self.address = address # Client Address 20 | self.__cmd_responses = {} # All "response" message types, key of dict is message's 'id' field 21 | self.__send_queue = [] # Message that are to be sent back to client, polled by self.__send() method 22 | self.__response_storage_lifespan = 360 # Max time (seconds) after which response is deleted from _cmd_responses 23 | self.__use_host_send_queue = False # If true also send fwd-host messages to client from self.__send() method 24 | self.stop_listening = False # If set to true self.__send() and self.__receive() will stop and terminate 25 | self.__eom = cfgvars.config["eom"] 26 | # Str which denotes end of a message block, must be same on client and server 27 | self.host_fwd_timeout = 20 # Time for which messages queued for will be waited for execution before removal 28 | 29 | def __send_host_response(self, message): 30 | message_id = message["id"] 31 | logger.info("[ClientID: %s] Forwarding message to host client: MSG_ID: %s", self.name, message_id) 32 | cfgvars.cmd_queue_host_only.append(message) 33 | cfgvars.cmd_host_only_ids.append(message_id) 34 | sent_at = int(time.time()) 35 | logger.debug("[ClientID: %s] Waiting for reply from host: MSG_ID: %s", self.name, message_id) 36 | while int(time.time()) < sent_at + self.host_fwd_timeout: 37 | if message_id in cfgvars.cmd_host_only_responses: 38 | self.__send_queue.append(create_reply( 39 | message, 40 | cfgvars.cmd_host_only_responses[message_id]["data"], 41 | cfgvars.cmd_host_only_responses[message_id]["status"] 42 | )) 43 | logger.error("[ClientID: %s] Host replied to message : MSG_ID: %s", self.name, message_id) 44 | cfgvars.cmd_host_only_responses.pop(message_id) 45 | return True 46 | time.sleep(1) 47 | # We timed out, no response from host, remove the message from queue and send client message that it failed 48 | if message in cfgvars.cmd_queue_host_only: 49 | cfgvars.cmd_queue_host_only.remove(message) 50 | cfgvars.cmd_host_only_ids.remove(message_id) 51 | logger.error("[ClientID: %s] Host send no reply for message: MSG_ID: %s", self.name, message_id) 52 | self.__send_queue.append(create_reply( 53 | message, 54 | "No response from host. Timed out after : {} seconds".format(self.host_fwd_timeout), 55 | False 56 | )) 57 | return False 58 | 59 | # Use loop and thread instead of just a send call because this not only sends message but also checks if any client 60 | # have left message for host client too, we we don't constantly look for message to host, maybe the file requested 61 | # to be opened on host system will open after an hour or more ! 62 | def __send(self): 63 | while not self.stop_listening: 64 | try: 65 | for message in self.__send_queue: 66 | logger.debug("[ClientID: %s] Got message in queue. MSG_ID: %s", self.name, message["id"]) 67 | # Send pending message to client itself, mostly response of request made by client 68 | message_json = json.dumps(message) + self.__eom 69 | self.session.sendall(message_json.encode()) 70 | logger.info("[ClientID: %s] Sent message to client. MSG_ID: %s", self.name, message["id"]) 71 | self.__send_queue.remove(message) 72 | # If client also accepts message from host only queue (message from other clients to this client, which 73 | # identifies as host system ) 74 | if self.__use_host_send_queue: 75 | for message in cfgvars.cmd_queue_host_only: 76 | message_json = json.dumps(message) + self.__eom 77 | self.session.sendall(message_json.encode()) 78 | logger.info( 79 | "[ClientID: %s] Client is host, Fwding host only messages. MSG_ID: %s", 80 | self.name, message["id"] 81 | ) 82 | cfgvars.cmd_queue_host_only.remove(message) 83 | time.sleep(0.01) 84 | except (ConnectionResetError, KeyboardInterrupt): 85 | self.stop_listening = True 86 | except: 87 | logger.error("[ClientID: %s] Unknown error while listening for messages: `%s`", traceback.format_exc()) 88 | self.stop_listening = True 89 | self.session.close() 90 | logger.debug("[ClientID: %s] Sender is exiting ", self.name) 91 | return True 92 | 93 | def __receive(self): 94 | while not self.stop_listening: 95 | message = b"" 96 | while not self.stop_listening: 97 | try: 98 | message = message + self.session.recv(16000) 99 | if message.endswith(self.__eom.encode()) or message == b"": 100 | break 101 | except (ConnectionResetError, KeyboardInterrupt): 102 | logger.error("[Client: %s] Client disconnected or keyboard interrupt received", self.name) 103 | self.stop_listening = True 104 | except: 105 | logger.error("[ClientID: %s] Unknown error while listening for messages: `%s`", self.name, 106 | traceback.format_exc()) 107 | self.stop_listening = True 108 | self.session.close() 109 | 110 | if message == b"" or self.stop_listening: 111 | self.stop_listening = True 112 | self.session.close() 113 | break 114 | try: 115 | message = json.loads(message.decode("utf-8").replace(self.__eom, "")) 116 | if message["type"] == "response": 117 | logger.info("[ClientID: %s] Received a response to message : MSG_ID: %s", self.name, message["id"]) 118 | message["received_on"] = int(time.time()) 119 | if message["id"] in cfgvars.cmd_host_only_ids: 120 | # This is a reply to command requested by different client, put it in globally 121 | # accessible variable 122 | print("Got a reply to host forwarded message") 123 | cfgvars.cmd_host_only_responses[message["id"]] = message 124 | cfgvars.cmd_host_only_ids.remove(message["id"]) 125 | else: 126 | self.__cmd_responses[message["id"]] = message 127 | elif message["type"] == "request": 128 | logger.info("[ClientID: %s] Received a request : MSG_ID: %s", self.name, message["id"]) 129 | if message["command"][0] == "fwd-host": 130 | logger.info("[ClientID: %s] Received a forward to host request : MSG_ID: %s", self.name, 131 | message["id"]) 132 | # Send message to host, wait for host response, send response back to this client 133 | # But first remove fwd-host command before sending to host else it will fail 134 | message["command"].pop(0) 135 | self.__send_host_response(message) 136 | elif message["command"][0] == "declare-self-host": 137 | self.__use_host_send_queue = True 138 | logger.info("[ClientID: %s] Declared itself as host.. " 139 | "This client will now receive messages forwarded to host", self.name) 140 | self.__send_queue.append(create_reply( 141 | message, 142 | "This client will now receive messages forwarded to host", 143 | True 144 | )) 145 | elif message["command"][0] in cfgvars.commands: 146 | # A valid command to run was received, run the command and put response to send queue 147 | # Which will be sent back to client 148 | logger.info("[ClientID: %s] Request handled by : %s", self.name, 149 | cfgvars.commands[message["command"][0]]) 150 | handler_name = cfgvars.commands[message["command"][0]] 151 | status, data = cfgvars.commands_handlers[handler_name].run_cmd(message["command"]) 152 | self.__send_queue.append(create_reply(message, data, status)) 153 | else: 154 | # This is unsupported command, send the reply back to client 155 | logger.debug("[ClientID: %s] Got unsupported command: Message body: `%s`", self.name, message) 156 | self.__send_queue.append(create_reply( 157 | message, 158 | "No instruction for command: '{}' ".format(message["command"][0]), 159 | False 160 | )) 161 | else: 162 | logger.debug("[ClientID: %s] Message type error: Message body: `%s`", self.name, message) 163 | self.__send_queue.append(create_reply( 164 | message, 165 | "Unsupported message type: '{}' ".format(message["type"]), 166 | False 167 | )) 168 | except (json.JSONDecodeError, KeyError, IndexError): 169 | logger.error("[ClientID: %s] Received a deformed message. Message body : '%s', Traceback : %s", 170 | self.name, 171 | message, 172 | traceback.format_exc() 173 | ) 174 | self.__send_queue.append(create_reply( 175 | {"id": "--", "type": "response"}, 176 | "Invalid message format", 177 | False 178 | )) 179 | except Exception as e: 180 | logger.error("[ClientID: %s] Unknown error while listening for messages: `%s`", self.name, 181 | traceback.format_exc()) 182 | self.stop_listening = True 183 | self.session.close() 184 | logger.debug("[ClientID: %s] Receiver is exiting ", self.name) 185 | return True 186 | 187 | def run(self): 188 | receiver = threading.Thread(target=self.__receive) 189 | sender = threading.Thread(target=self.__send) 190 | receiver.daemon = True 191 | sender.daemon = True 192 | # Start Threads 193 | receiver.start() 194 | sender.start() 195 | 196 | 197 | def start_server(host, port): 198 | try: 199 | server = socket.socket() 200 | server.settimeout(3) 201 | server.bind((host, port)) 202 | server.listen(5) 203 | clients = [] 204 | while True: 205 | try: 206 | session, address = server.accept() 207 | clients.append(ClientConnectionThread(randomstr(8), session, address)) 208 | clients[len(clients) - 1].run() 209 | # Now check if any client has paused the work and remove them 210 | for client in clients: 211 | if client.stop_listening: 212 | logger.debug("Client Thread '%s' has stopped listening, removing it", client.name) 213 | del clients[clients.index(client)] 214 | except socket.timeout: 215 | pass 216 | except KeyboardInterrupt: 217 | logger.debug("Got keyboard interrupt") 218 | if server is not None: 219 | server.close() 220 | logger.info("Server is stopping !") 221 | sys.exit(1) 222 | -------------------------------------------------------------------------------- /buildall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "==> Building linux component" 3 | cd app-linux 4 | ./build.sh 5 | echo "==> Done" 6 | cd .. 7 | echo "==> Building windows component" 8 | cd app-win 9 | ./build.sh 10 | echo "==> Done" 11 | -------------------------------------------------------------------------------- /docs/1-virt-manager.md: -------------------------------------------------------------------------------- 1 | # 1. Setting up Windows vm with virt-manager and KVM 2 | 3 | This will help you set up an efficient virtual machine for use with cassowary. 4 | 5 | The instructions are written mainly for Arch Linux, so be sure to adapt them to the distro you are using! 6 | 7 | ### We start by installing virt-manager and KVM 8 | 9 | ```bash 10 | $ sudo pacman -S virt-manager 11 | ``` 12 | 13 | ### Making KVM run without root access 14 | 15 | ```bash 16 | $ sudo sed -i "s/#user = \"root\"/user = \"$(id -un)\"/g" /etc/libvirt/qemu.conf 17 | $ sudo sed -i "s/#group = \"root\"/group = \"$(id -gn)\"/g" /etc/libvirt/qemu.conf 18 | $ sudo usermod -a -G kvm $(id -un) 19 | $ sudo usermod -a -G libvirt $(id -un) 20 | $ sudo systemctl restart libvirtd 21 | $ sudo ln -s /etc/apparmor.d/usr.sbin.libvirtd /etc/apparmor.d/disable/ 22 | ``` 23 | 24 | ### Making network available with AppArmor enabled 25 | 26 | On some Linux distribution if AppArmor is enabled it is necessary to modify the file `/etc/apparmor.d/usr.sbin.dnsmasq` to be able to connect to the network or virt-manager will throw a segmentation fault. 27 | 28 | If you can't find the `dnsmasq` profile, be sure to install every additional packages regarding AppArmor profiles. 29 | If you can't still find the `dnsmasq` profile, you can always download it from [AppArmor gitlab](https://gitlab.com/apparmor/apparmor/-/raw/master/profiles/apparmor.d/usr.sbin.dnsmasq) and copy it to the right location: 30 | 31 | ```bash 32 | $ wget https://gitlab.com/apparmor/apparmor/-/raw/master/profiles/apparmor.d/usr.sbin.dnsmasq -O ~/usr.sbin.dnsmasq 33 | $ sudo mv ~/usr.sbin.dnsmasq /etc/apparmor.d/ 34 | ``` 35 | 36 | Now, to modify the main profile, you will have to add a `r` at the end of the line about `libvirt_leaseshelper` , so it will be like: `/usr/libexec/libvirt_leaseshelper mr,`. 37 | 38 | This can also be done via terminal: 39 | 40 | ```bash 41 | $ sudo sed -i "s/\/usr\/libexec\/libvirt_leaseshelper m,/\/usr\/libexec\/libvirt_leaseshelper mr,/g" /etc/apparmor.d/usr.sbin.dnsmasq 42 | ``` 43 | 44 | Remember that those changes should be repeated on every AppArmor update. 45 | 46 | ### Create libvirt.conf 47 | 48 | On some Linux distribution is better to create a config file to make sure the default libvirt uri is the system one. 49 | 50 | To do this create the folder `~/.config/libvirt/` and inside this folder create the `libvirt.conf` with `uri_default = "qemu:///system"` 51 | 52 | ```bash 53 | $ mkdir -p ~/.config/libvirt 54 | $ echo "uri_default = \"qemu:///system\"" >> ~/.config/libvirt/libvirt.conf 55 | ``` 56 | 57 | Now you will need to restart for all the changes to take place. 58 | 59 | ### Download Windows .iso image and VirtIO Drivers for Windows 60 | 61 | We will need either Windows 7, 8, or 10 Pro, Enterprise or Server to use RDP apps. 62 | VirtIO driver will improve the VM performance while having lowest overhead. 63 | 64 | > **BE SURE TO USE AN ADBLOCK FOR THE SITE BELOW!** 65 | > Download links are generated directly from Microsoft CDN, so they are totally legit. 66 | 67 | - Download Windows isos from: [HERE](https://tb.rg-adguard.net/public.php) 68 | 69 | - Download latest VirtIO driver iso images from: [HERE](https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso) 70 | 71 | > If you are using Windows 7 you don't need to download VirtIO iso as it is not supported. 72 | 73 | and save them in a convenient location. 74 | 75 | ### Creating a Virtual Machine 76 | 77 | - Open virt-manager from your application menu 78 | 79 | - On virt-manager click on **Edit** -> **Preferences**, check **Enable XML editing** then click Close; 80 | 81 | virt-manager-0 82 | 83 | - From virt-manager menu bar select **File** and then **New Virtual Machine**; 84 | 85 | - On the New VM window select **Local Install media** and click Next; 86 | 87 | virt-manager-1 88 | 89 | - Browse and select the Windows 10 iso you downloaded on install media then click Next again; 90 | 91 | - Set the CPU cores (2 recommended), Memory (4096 MB recommended) and Disk Space as per your preferences; 92 | 93 | virt-manager-2 94 | virt-manager-3 95 | 96 | - Give a name to your vm such as `Win10` and check **Customize configuration before install** then click on Finish!; 97 | 98 | virt-manager-4 99 | 100 | - In the CPU tab make sure **Copy host configuration** is checked; 101 | 102 | virt-manager-5 103 | 104 | - Goto XML tab of CPU and replace the section: 105 | 106 | ```xml 107 | 108 | ....... 109 | ....... 110 | 111 | ``` 112 | 113 | with: 114 | 115 | ```xml 116 | 117 | 118 | 119 | 120 | ``` 121 | 122 | - In the Memory tab set the **Current allocation** to **1024**, so the VM won't use 4GiB of memory directly but it will range from 1GiB to 4GiB; 123 | 124 | virt-manager-6 125 | 126 | - In the Boot Options tab you could check **Start the virtual machine on host bootup** if you would like the VM to boot automatically at your PC boot; 127 | 128 | - In the SATA Disk 1 tab set the **Disk bus** to **VirtIO**; 129 | 130 | > If you are using Windows 7 skip this step as VirtIO is not supported. 131 | 132 | virt-manager-7 133 | 134 | - Move over to NIC section and set **Device model** to **virtio**; 135 | 136 | virt-manager-8 137 | 138 | - Click on **Add hardware** at the bottom left, select **Storage** then choose **Select or Create custom storage**; click on **Manage**, browse and select the downloaded virtio-win driver iso. Finally set the device type to **CDROM** and click on Finish; 139 | 140 | virt-manager-9 141 | 142 | - Click **Begin Installation** on top left; 143 | 144 | - Follow the installation instructions for Windows 10 and when choosing a Custom installation you will get no drives to install Windows 10 on. To make the VirtIO drive works you will have to click on **Load Driver**, then choose **OK** and finally select the driver for Windows 10; 145 | 146 | > If no drivers are loaded or shown, let Windows search for them inside the `amd64` folder of the VirtIO disk. 147 | 148 | virt-manager-10 149 | 150 | - After that your drive will show and you can continue like a normal Windows 10 installation; 151 | - After some time you will get to "Let's connect to internet page", click on **I don't have internet** at bottom left and continue with limited setup; 152 | - Set your username and password. The Password is not allowed to be blank; 153 | - After you get to Windows 10 desktop open This PC and browse to virtio-win CD drive and install **virtio-win-gt-x64.exe**; 154 | - It's also suggested to install the [spice guest tools](https://www.spice-space.org/download/windows/spice-guest-tools/spice-guest-tools-latest.exe) to also enable copy-paste between host and guest; 155 | - Shut down the VM and from the menubar select **View** and then **Details**; 156 | - Go to Display Spice section and set **Listen Type** to **None**; also check the OpenGL option and click Apply; 157 | - Go to Video QXL section and set **Model** to **VirtIO** and check the 3D acceleration option; 158 | 159 | > If you get a black screen after those changes, revert those changes. This could happen with nvidia graphics card; 160 | 161 | - Start the VM by clicking the play button on the top left (you may need to click the Monitor icon to show the VM screen ). Login to desktop; 162 | - Open up edge and browse to this page and continue the instructions for installing cassowary. 163 | 164 | --- 165 | 166 | Note: For better 3D performance you can use VMware or other virtualization platforms, ( The IP autodetection and VM auto suspend only works for libvirt based platforms as of now. 167 | 168 | --- 169 | 170 | **Next guide** -> [Installing cassowary on Windows guest and Linux host](2-cassowary-install.md) 171 | -------------------------------------------------------------------------------- /docs/2-cassowary-install.md: -------------------------------------------------------------------------------- 1 | # 2. Installing cassowary on Windows (guest) and Linux (host) 2 | 3 | ## On Windows (guest) 4 | 5 | - Open Settings, click on System, scroll to bottom and click on Remote desktop then click on Enable Remote Desktop and click confirm! 6 | - Open this page and download latest .zip from the [release page](https://github.com/casualsnek/cassowary/releases/) 7 | - Extract the downloaded .zip file 8 | - Double click on `setup.bat` located on extracted directory 9 | - Logout and login again to windows session 10 | - After you have logged in continue the instructions below 11 | 12 | ## On Linux (host) 13 | 14 | Here we will be using Arch Linux. You can easily find equivalent commands for your Linux distro. 15 | 16 | - Go to the [release page](https://github.com/casualsnek/cassowary/releases/) and download latest `.whl`; 17 | - Open terminal on the location where you downloaded the `.whl` file; 18 | - Install `python3`, `python3-pip`, `freerdp2`, `libvirt-python3` packages and dependencies by running following commands on terminal: 19 | 20 | ```bash 21 | $ sudo pacman -S python3 python3-pip freerdp libvirt-python 22 | $ pip3 install PyQt5 23 | ``` 24 | 25 | - Install the downloaded `.whl` file by running: 26 | 27 | ```bash 28 | $ pip install cassowary* 29 | ``` 30 | 31 | > If you get any warning about the folder `/home/$USER/.local/bin` not in your PATH, you can easily add it by adding it to your `$HOME/.profile` or `$HOME/.bash_profile`: 32 | > 33 | > ``` 34 | > echo "PATH=\$PATH:$HOME/.local/bin" >> $HOME/.profile 35 | > ``` 36 | 37 | - Launch cassowary configuration utility with: 38 | 39 | ```bash 40 | $ python3 -m cassowary -a 41 | ``` 42 | 43 | - Head over to Misc tab and click on **"Setup Autostart"** and **"Create"**. This will bring cassowary to your application menu and setup background service autostart; 44 | - Enter the VM name from the VM setup step; in this case `Win10`; 45 | - Click on "Save changes" and then on **"Autodetect"**, this should automatically fill up the VM IP; 46 | - Click "Save changes" again then click **"Autofill host/username"** then enter the password you set during the windows setup. Then click "Save changes" again; 47 | - Now goto **"Guest app"** tab and create shortcut for any application you want. 48 | 49 | Now you can find application on your application menu which you can use to launch apps easily 50 | You can explore other commandline usage of cassowary by running: 51 | 52 | ```bash 53 | $ python -m cassowary -h 54 | ``` 55 | 56 | --- 57 | 58 | [3. Extra How to's and FAQ](3-faq.md) 59 | -------------------------------------------------------------------------------- /docs/3-faq.md: -------------------------------------------------------------------------------- 1 | # 3. Extra How to(s) and FAQ(s) 2 | 3 | **Q. Launch terminal/Open on host on Windows file manager says the drive is not shared?** 4 | 5 | A. Open cassowary on Linux, go to **"Folder Mapping"** tab, go to **"Windows->Linux"** sub tab then create a new share for the drive where the file is located then click on mount all. 6 | 7 | --- 8 | 9 | **Q. How do I share my folder on Linux to Windows as local drive ?** 10 | 11 | A. Open cassowary on Linux, go to **"Folder Mapping"** tab Goto **"Linux->Windows"** sub tab then click on **Create new file**, give name to share, browse location, choose drive letter then click on create map. 12 | 13 | --- 14 | 15 | **Q. How to launch Windows application that is not listed on guest app by path on Windows?** 16 | 17 | A. Run: 18 | 19 | ```bash 20 | $ python3 -m cassowary -c guest-run -- {path to app and arguments} 21 | ``` 22 | 23 | --- 24 | 25 | **Q. How do I set links and files on Windows to open on Linux host application?** 26 | 27 | A. Set the default app for a file type or default browser to `C:\Program Files\cassowary\hostopen.bat`( if file type 'Launch on host' tab is recommended way to set this up ) 28 | 29 | --- 30 | 31 | **Q. How do I set to launch a file type on Linux on Windows application?** 32 | 33 | A. Most Linux system allow setting default application for file type, create a Application menu entry for the app you want and set default application to the created desktop application. 34 | 35 | --- 36 | 37 | **Q. I setup everything but get a connection error which launching cassowary Linux?** 38 | 39 | A. Since cassowary won't launch without a user being logged in, try launching any windows application and click reconnect ! 40 | 41 | --- 42 | 43 | **Q. Open on host or open host terminal does not work?** 44 | 45 | A. Make sure you have setup background service autostart (logout and login is required after clicking on the setup button). You can also try manually launching background service using: 46 | 47 | ```bash 48 | $ python -m cassowary -bc 49 | ``` 50 | 51 | --- 52 | 53 | **Q. Setting file extension on 'Launch on host' does not automatically open it with host application.** 54 | 55 | A. Make sure background service shortcut is created and is running, For an extension with app to open it already installed will cause windows to show dialog to choose default app. Select 'Windows script host' on the shown dialog. 56 | 57 | --- 58 | 59 | **Q. New application does not create a taskbar entry.** 60 | 61 | A. Disable taskbar application grouping, Also, If you are using v0.6+, try navigating to advanced tab and disable 'Prefer using server to launch applications' this may solve this issue ! 62 | 63 | --- 64 | 65 | **Q. There is no internet in VM.** 66 | 67 | A. This happens if you have a VPN running on host, try disabling VPN and internet should work on VM ! 68 | 69 | --- 70 | 71 | **Q. I have found a bug/issue, have a suggestion, or have questions not answered here!** 72 | 73 | A. Feel free to [open an issue here](https://github.com/casualsnek/cassowary/issues)! 74 | -------------------------------------------------------------------------------- /docs/img/app-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/app-preview.gif -------------------------------------------------------------------------------- /docs/img/virt-manager-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-0.png -------------------------------------------------------------------------------- /docs/img/virt-manager-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-1.png -------------------------------------------------------------------------------- /docs/img/virt-manager-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-10.png -------------------------------------------------------------------------------- /docs/img/virt-manager-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-2.png -------------------------------------------------------------------------------- /docs/img/virt-manager-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-3.png -------------------------------------------------------------------------------- /docs/img/virt-manager-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-4.png -------------------------------------------------------------------------------- /docs/img/virt-manager-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-5.png -------------------------------------------------------------------------------- /docs/img/virt-manager-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-6.png -------------------------------------------------------------------------------- /docs/img/virt-manager-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-7.png -------------------------------------------------------------------------------- /docs/img/virt-manager-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-8.png -------------------------------------------------------------------------------- /docs/img/virt-manager-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casualsnek/cassowary/e04d5afe269576084e06c65b98d2f29f59b9bbda/docs/img/virt-manager-9.png --------------------------------------------------------------------------------