├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── include └── purespice.h ├── refresh-copyright ├── src ├── agent.c ├── agent.h ├── channel.c ├── channel.h ├── channel_cursor.c ├── channel_cursor.h ├── channel_display.c ├── channel_display.h ├── channel_inputs.c ├── channel_inputs.h ├── channel_main.c ├── channel_main.h ├── channel_playback.c ├── channel_playback.h ├── channel_record.c ├── channel_record.h ├── draw.h ├── locking.h ├── log.c ├── log.h ├── messages.h ├── ps.c ├── ps.h ├── queue.c ├── queue.h ├── rsa.c └── rsa.h └── test ├── CMakeLists.txt └── main.c /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: gnif 4 | patreon: gnif 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: lookingglass 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | linux: 5 | runs-on: ubuntu-20.04 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | cc: [gcc, clang] 10 | build_type: [Release, Debug] 11 | steps: 12 | - uses: actions/checkout@v1 13 | with: 14 | submodules: recursive 15 | - name: Update apt 16 | run: | 17 | sudo apt-get update 18 | - name: Install PureSpice dependencies 19 | run: | 20 | sudo apt-get install libspice-protocol-dev nettle-dev 21 | - name: Configure PureSpice 22 | env: 23 | CC: /usr/bin/${{ matrix.cc }} 24 | run: | 25 | mkdir build 26 | cd build 27 | cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} .. 28 | - name: Build PureSpice 29 | run: | 30 | cd build 31 | make -j$(nproc) 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.a 2 | *.o 3 | *.exe 4 | */build 5 | build 6 | *.swp 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/ADL"] 2 | path = test/ADL 3 | url = https://github.com/gnif/ADL.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(purespice LANGUAGES C) 3 | set(CMAKE_C_STANDARD 11) 4 | 5 | find_package(PkgConfig) 6 | pkg_check_modules(SPICE_PKGCONFIG REQUIRED 7 | spice-protocol 8 | nettle 9 | hogweed 10 | ) 11 | 12 | add_definitions(-D USE_NETTLE) 13 | 14 | add_compile_options( 15 | "-Wall" 16 | "-Wextra" 17 | "-Werror" 18 | "-Wfatal-errors" 19 | "-Wstrict-prototypes" 20 | ) 21 | add_library(purespice STATIC 22 | src/ps.c 23 | src/log.c 24 | src/rsa.c 25 | src/queue.c 26 | src/channel.c 27 | src/channel_main.c 28 | src/channel_inputs.c 29 | src/channel_playback.c 30 | src/channel_record.c 31 | src/channel_display.c 32 | src/channel_cursor.c 33 | src/agent.c 34 | ) 35 | 36 | target_link_libraries(purespice 37 | ${SPICE_PKGCONFIG_LIBRARIES} 38 | gmp 39 | ) 40 | 41 | target_include_directories(purespice 42 | PUBLIC 43 | $ 44 | $ 45 | PRIVATE 46 | src 47 | ${SPICE_PKGCONFIG_INCLUDE_DIRS} 48 | ) 49 | -------------------------------------------------------------------------------- /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 | # PureSpice 2 | 3 | A pure C implementation of the spice protocol as used by the Looking Glass 4 | project. This implementation unlike libspice does not require or rely on glib in 5 | any way. 6 | 7 | ## Note 8 | 9 | This project's goal has been to provide keyboard and mouse input for the Looking 10 | Glass project where the host and client run on the same physical system and as 11 | such is missing encryption support. 12 | 13 | ## Donations 14 | 15 | I (Geoffrey McRae) am the primary developer behind this project and I have 16 | invested countless of hours of development time into it. 17 | 18 | If you like this project and find it useful and would like to help out you can 19 | support me directly using the following platforms. 20 | 21 | * [GitHub](https://github.com/sponsors/gnif) 22 | * [Ko-Fi](https://ko-fi.com/lookingglass) 23 | * [Patreon](https://www.patreon.com/gnif) 24 | * [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ESQ72XUPGKXRY) 25 | * BTC - 14ZFcYjsKPiVreHqcaekvHGL846u3ZuT13 26 | -------------------------------------------------------------------------------- /include/purespice.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef PURE_SPICE_H__ 22 | #define PURE_SPICE_H__ 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | typedef enum PSStatus 29 | { 30 | PS_STATUS_RUN, 31 | PS_STATUS_SHUTDOWN, 32 | PS_STATUS_ERR_POLL, 33 | PS_STATUS_ERR_READ, 34 | PS_STATUS_ERR_ACK 35 | } 36 | PSStatus; 37 | 38 | typedef enum PSDataType 39 | { 40 | SPICE_DATA_TEXT, 41 | SPICE_DATA_PNG, 42 | SPICE_DATA_BMP, 43 | SPICE_DATA_TIFF, 44 | SPICE_DATA_JPEG, 45 | 46 | SPICE_DATA_NONE 47 | } 48 | PSDataType; 49 | 50 | typedef enum PSAudioFormat 51 | { 52 | PS_AUDIO_FMT_INVALID, 53 | PS_AUDIO_FMT_S16 54 | } 55 | PSAudioFormat; 56 | 57 | typedef struct PSServerInfo 58 | { 59 | char * name; 60 | uint8_t uuid[16]; 61 | } 62 | PSServerInfo; 63 | 64 | typedef enum PSChannelType 65 | { 66 | PS_CHANNEL_MAIN, 67 | PS_CHANNEL_INPUTS, 68 | PS_CHANNEL_PLAYBACK, 69 | PS_CHANNEL_RECORD, 70 | PS_CHANNEL_DISPLAY, 71 | PS_CHANNEL_CURSOR, 72 | 73 | PS_CHANNEL_MAX 74 | } 75 | PSChannelType; 76 | 77 | typedef enum PSSurfaceFormat 78 | { 79 | PS_SURFACE_FMT_1_A, 80 | PS_SURFACE_FMT_8_A, 81 | PS_SURFACE_FMT_16_555, 82 | PS_SURFACE_FMT_32_xRGB, 83 | PS_SURFACE_FMT_16_565, 84 | PS_SURFACE_FMT_32_ARGB 85 | } 86 | PSSurfaceFormat; 87 | 88 | typedef enum PSBitmapFormat 89 | { 90 | PS_BITMAP_FMT_1BIT_LE, 91 | PS_BITMAP_FMT_1BIT_BE, 92 | PS_BITMAP_FMT_4BIT_LE, 93 | PS_BITMAP_FMT_4BIT_BE, 94 | PS_BITMAP_FMT_8BIT, 95 | PS_BITMAP_FMT_16BIT, 96 | PS_BITMAP_FMT_24BIT, 97 | PS_BITMAP_FMT_32BIT, 98 | PS_BITMAP_FMT_RGBA, 99 | PS_BITMAP_FMT_8BIT_A 100 | } 101 | PSBitmapFormat; 102 | 103 | typedef enum PSRopd 104 | { 105 | PS_ROPD_INVERS_SRC, 106 | PS_ROPD_INVERS_BRUSH, 107 | PS_ROPD_INVERS_DEST, 108 | PS_ROPD_OP_PUT, 109 | PS_ROPD_OP_OR, 110 | PS_RPOD_OP_AND, 111 | PS_ROPD_OP_XOR, 112 | PS_ROPD_OP_BLACKNESS, 113 | PS_ROPD_OP_WHITENESS, 114 | PS_ROPD_OP_INVERS, 115 | PS_ROPD_INVERS_RES 116 | } 117 | PSRopd; 118 | 119 | typedef struct PSInit 120 | { 121 | struct 122 | { 123 | void (*info)(const char * file, unsigned int line, const char * function, 124 | const char * format, ...) __attribute__((format (printf, 4, 5))); 125 | 126 | void (*warn)(const char * file, unsigned int line, const char * function, 127 | const char * format, ...) __attribute__((format (printf, 4, 5))); 128 | 129 | void (*error)(const char * file, unsigned int line, const char * function, 130 | const char * format, ...) __attribute__((format (printf, 4, 5))); 131 | } 132 | log; 133 | } 134 | PSInit; 135 | 136 | typedef struct PSConfig 137 | { 138 | const char * host; 139 | unsigned port; 140 | const char * password; 141 | 142 | /* [optional] called once the connection is ready (all channels connected) */ 143 | void (*ready)(void); 144 | 145 | struct 146 | { 147 | /* enable input support if available */ 148 | bool enable; 149 | 150 | /* automatically connect to the channel as soon as it's available */ 151 | bool autoConnect; 152 | } 153 | inputs; 154 | 155 | struct 156 | { 157 | /* enable clipboard support if available */ 158 | bool enable; 159 | 160 | /* called with the data type available by the agent */ 161 | void (*notice)(const PSDataType type); 162 | 163 | /* called with the clipboard data */ 164 | void (*data)(const PSDataType type, uint8_t * buffer, uint32_t size); 165 | 166 | /* called to notify that there is no longer any clipboard data available */ 167 | void (*release)(void); 168 | 169 | /* called to request clipboard data of the specified type */ 170 | void (*request)(const PSDataType type); 171 | } 172 | clipboard; 173 | 174 | struct 175 | { 176 | /* enable the playback channel if available */ 177 | bool enable; 178 | 179 | /* automatically connect to the channel as soon as it's available */ 180 | bool autoConnect; 181 | 182 | /* called with the details of the stream to open */ 183 | void (*start)(int channels, int sampleRate, PSAudioFormat format, 184 | uint32_t time); 185 | 186 | /* [optional] called with the volume of each channel to set */ 187 | void (*volume)(int channels, const uint16_t volume[]); 188 | 189 | /* [optional] called to mute/unmute the stream */ 190 | void (*mute)(bool mute); 191 | 192 | /* called when the guest stops the audio stream */ 193 | void (*stop)(void); 194 | 195 | /* called when there are audio samples */ 196 | void (*data)(uint8_t * data, size_t size); 197 | } 198 | playback; 199 | 200 | struct 201 | { 202 | /* enable the playback channel if available */ 203 | bool enable; 204 | 205 | /* automatically connect to the channel as soon as it's available */ 206 | bool autoConnect; 207 | 208 | /* called with the details of the stream to open */ 209 | void (*start)(int channels, int sampleRate, PSAudioFormat format); 210 | 211 | /* [optional] called with the volume of each channel to set */ 212 | void (*volume)(int channels, const uint16_t volume[]); 213 | 214 | /* [optional] called to mute/unmute the stream */ 215 | void (*mute)(bool mute); 216 | 217 | /* called when the guest stops the audio stream */ 218 | void (*stop)(void); 219 | } 220 | record; 221 | 222 | struct 223 | { 224 | /* enable the display channel if available */ 225 | bool enable; 226 | 227 | /* automatically connect to the channel as soon as it's available */ 228 | bool autoConnect; 229 | 230 | /* called to create a new surface */ 231 | void (*surfaceCreate)(unsigned int surfaceId, PSSurfaceFormat format, 232 | unsigned int width, unsigned int height); 233 | 234 | /* called to destroy a surface */ 235 | void (*surfaceDestroy)(unsigned int surfaceId); 236 | 237 | /* called to draw a bitmap to a surface */ 238 | void (*drawBitmap)(unsigned int surfaceId, 239 | PSBitmapFormat format, 240 | bool topDown, 241 | int x , int y, 242 | int width, int height, 243 | int stride, 244 | void * data); 245 | 246 | /* called to fill an area with a color */ 247 | void (*drawFill)(unsigned int surfaceId, 248 | int x , int y, 249 | int width, int height, 250 | uint32_t color); 251 | } 252 | display; 253 | 254 | struct 255 | { 256 | /* enable the cursor channel if available */ 257 | bool enable; 258 | 259 | /* automatically connect to the channel as soon as it's available */ 260 | bool autoConnect; 261 | 262 | /* called to indicate the cursor image has changed to an RGBA bitmap 263 | * with hotspot at (hx, hy) */ 264 | void (*setRGBAImage)(int width, int height, int hx, int hy, 265 | const void * data); 266 | 267 | /* called to indicate the cursor image has changed to an monochrome bitmap 268 | * with hotspot at (hx, hy) */ 269 | void (*setMonoImage)(int width, int height, int hx, int hy, 270 | const void * xorMask, const void * andMask); 271 | 272 | /* called to indicate that the mouse position is set as follows */ 273 | void (*setState)(bool visible, int x, int y); 274 | 275 | /* called to indicate that the mouse trail is set as follows (optional) */ 276 | void (*setTrail)(int length, int frequency); 277 | } 278 | cursor; 279 | } 280 | PSConfig; 281 | 282 | #ifdef __cplusplus 283 | extern "C" { 284 | #endif 285 | 286 | /* 287 | * Initialize the library for use, this may be called before any other methods 288 | * to setup the library. If not initialization will be automatically performed 289 | * by `purespice_connect` with default parameters. 290 | * 291 | * `init` is optional and may be NULL 292 | */ 293 | void purespice_init(const PSInit * init); 294 | 295 | bool purespice_connect(const PSConfig * config); 296 | void purespice_disconnect(void); 297 | PSStatus purespice_process(int timeout); 298 | 299 | bool purespice_getServerInfo(PSServerInfo * info); 300 | void purespice_freeServerInfo(PSServerInfo * info); 301 | 302 | bool purespice_hasChannel (PSChannelType channel); 303 | bool purespice_channelConnected (PSChannelType channel); 304 | bool purespice_connectChannel (PSChannelType channel); 305 | bool purespice_disconnectChannel(PSChannelType channel); 306 | 307 | bool purespice_keyDown (uint32_t code); 308 | bool purespice_keyUp (uint32_t code); 309 | bool purespice_keyModifiers (uint32_t modifiers); 310 | bool purespice_mouseMode (bool server); 311 | bool purespice_mousePosition(uint32_t x, uint32_t y); 312 | bool purespice_mouseMotion ( int32_t x, int32_t y); 313 | bool purespice_mousePress (uint32_t button); 314 | bool purespice_mouseRelease (uint32_t button); 315 | 316 | bool purespice_clipboardRequest(PSDataType type); 317 | bool purespice_clipboardGrab(PSDataType types[], int count); 318 | bool purespice_clipboardRelease(void); 319 | 320 | bool purespice_clipboardDataStart(PSDataType type, size_t size); 321 | bool purespice_clipboardData(PSDataType type, uint8_t * data, size_t size); 322 | 323 | bool purespice_writeAudio(void * data, size_t size, uint32_t time); 324 | 325 | #ifdef __cplusplus 326 | } 327 | #endif 328 | 329 | #endif /* PURE_SPICE_H__ */ 330 | -------------------------------------------------------------------------------- /refresh-copyright: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import fnmatch 5 | import os 6 | import re 7 | import subprocess 8 | from textwrap import wrap 9 | 10 | PROJECT = os.path.dirname(__file__) 11 | EXTENSIONS = ('.c', '.cpp', '.h', '.nsi', '.rc') 12 | START_YEAR = 2017 13 | CURRENT_YEAR = datetime.date.today().year 14 | 15 | reignore = re.compile('^vendor/|.*/shader/|.*/d3d12.h$') 16 | recopyright = re.compile(r'\A/\*.*?\*/\s+', re.DOTALL) 17 | 18 | project_name = 'PureSpice - A pure C implementation of the SPICE client protocol' 19 | copyright = f'Copyright © {START_YEAR}-{CURRENT_YEAR} Geoffrey McRae ' 20 | project_url = 'https://github.com/gnif/PureSpice' 21 | header = [project_name, copyright, project_url] 22 | 23 | paragraphs = ['''\ 24 | This program is free software; you can redistribute it and/or modify it 25 | under the terms of the GNU General Public License as published by the Free 26 | Software Foundation; either version 2 of the License, or (at your option) 27 | any later version.''', 28 | '''\ 29 | This program is distributed in the hope that it will be useful, but WITHOUT 30 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 31 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 32 | more details.''', 33 | '''\ 34 | You should have received a copy of the GNU General Public License along 35 | with this program; if not, write to the Free Software Foundation, Inc., 59 36 | Temple Place, Suite 330, Boston, MA 02111-1307 USA'''] 37 | 38 | 39 | def make_comment_block(): 40 | lines = ['/**'] 41 | lines += [' * ' + line for line in header] 42 | 43 | for paragraph in paragraphs: 44 | lines.append(' *') 45 | lines += wrap(paragraph, width=78, initial_indent=' * ', subsequent_indent=' * ') 46 | 47 | lines.append(' */') 48 | return '\n'.join(lines) + '\n\n' 49 | 50 | 51 | def gen_c_literal(): 52 | lines = [''] + header 53 | for paragraph in paragraphs: 54 | lines.append('') 55 | lines += wrap(paragraph, width=79) 56 | lines.append('') 57 | return '\n'.join(f' "{line}\\n"' for line in lines) + '\n' 58 | 59 | 60 | def update_c_style(file, copyright): 61 | print(f'Updating copyright for {file}...') 62 | with open(file, encoding='utf-8') as f: 63 | data = recopyright.sub('', f.read()) 64 | with open(file, 'w', encoding='utf-8') as f: 65 | f.write(copyright) 66 | f.write(data) 67 | 68 | def appstring_license(): 69 | lines = [] 70 | for paragraph in paragraphs: 71 | paragraph = wrap(paragraph, width=75) 72 | for line in paragraph[:-1]: 73 | lines.append(f' "{line} "') 74 | lines.append(f' "{paragraph[-1]}\\n"') 75 | lines.append(r' "\n"') 76 | lines.pop() 77 | lines[-1] = f'{lines[-1][:-3]}";' 78 | return lines 79 | 80 | def main(): 81 | comment_block = make_comment_block() 82 | files = subprocess.check_output(['git', '-C', PROJECT, 'ls-files', '-z']).decode('utf-8').split('\0') 83 | for file in files: 84 | if reignore.match(file): 85 | continue 86 | if file.endswith(EXTENSIONS): 87 | update_c_style(os.path.join(PROJECT, file), comment_block) 88 | 89 | if __name__ == '__main__': 90 | main() -------------------------------------------------------------------------------- /src/agent.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | 23 | #include "ps.h" 24 | #include "log.h" 25 | #include "channel.h" 26 | #include "channel_main.h" 27 | 28 | #include "messages.h" 29 | #include "rsa.h" 30 | #include "queue.h" 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include 38 | #include 39 | 40 | #include 41 | 42 | typedef struct PSAgent PSAgent; 43 | 44 | struct PSAgent 45 | { 46 | bool present; 47 | struct Queue * queue; 48 | atomic_uint serverTokens; 49 | 50 | // clipboard variables 51 | bool cbSupported; 52 | bool cbSelection; 53 | bool cbAgentGrabbed; 54 | bool cbClientGrabbed; 55 | PSDataType cbType; 56 | uint8_t * cbBuffer; 57 | uint32_t cbRemain; 58 | uint32_t cbSize; 59 | 60 | ssize_t msgSize; 61 | }; 62 | 63 | static PSAgent agent = {0}; 64 | 65 | static PS_STATUS agent_sendCaps(bool request); 66 | static void agent_onClipboard(void); 67 | static uint32_t psTypeToAgentType(PSDataType type); 68 | static PSDataType agentTypeToPSType(uint32_t type); 69 | 70 | bool agent_present(void) 71 | { 72 | return agent.present; 73 | } 74 | 75 | PS_STATUS agent_connect(void) 76 | { 77 | if (!agent.queue) 78 | agent.queue = queue_new(); 79 | else 80 | { 81 | void * msg; 82 | while(queue_shift(agent.queue, &msg)) 83 | SPICE_RAW_PACKET_FREE(msg); 84 | } 85 | 86 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_MAIN]; 87 | uint32_t * packet = SPICE_PACKET(SPICE_MSGC_MAIN_AGENT_START, uint32_t, 0); 88 | memcpy(packet, &(uint32_t){SPICE_AGENT_TOKENS_MAX}, sizeof(uint32_t)); 89 | if (!SPICE_SEND_PACKET(channel, packet)) 90 | { 91 | PS_LOG_ERROR("Failed to send SPICE_MSGC_MAIN_AGENT_START"); 92 | return PS_STATUS_ERROR; 93 | } 94 | 95 | agent.present = true; 96 | PS_STATUS ret = agent_sendCaps(true); 97 | if (ret != PS_STATUS_OK) 98 | { 99 | agent.present = false; 100 | PS_LOG_ERROR("Failed to send our capabillities to the spice guest agent"); 101 | return ret; 102 | } 103 | 104 | PS_LOG_INFO("Connected to the spice guest agent"); 105 | return PS_STATUS_OK; 106 | } 107 | 108 | void agent_disconnect(void) 109 | { 110 | if (agent.queue) 111 | { 112 | void * msg; 113 | while(queue_shift(agent.queue, &msg)) 114 | SPICE_RAW_PACKET_FREE(msg); 115 | queue_free(agent.queue); 116 | agent.queue = NULL; 117 | } 118 | 119 | if (agent.cbBuffer) 120 | { 121 | free(agent.cbBuffer); 122 | agent.cbBuffer = NULL; 123 | } 124 | 125 | agent.cbRemain = 0; 126 | agent.cbSize = 0; 127 | 128 | agent.cbAgentGrabbed = false; 129 | agent.cbClientGrabbed = false; 130 | 131 | agent.present = false; 132 | } 133 | 134 | #pragma pack(push,1) 135 | struct Selection 136 | { 137 | uint8_t selection; 138 | uint8_t reserved[3]; 139 | }; 140 | #pragma pack(pop) 141 | 142 | PS_STATUS agent_process(PSChannel * channel) 143 | { 144 | if (agent.cbRemain) 145 | { 146 | memcpy(agent.cbBuffer + agent.cbSize, channel->buffer, channel->header.size); 147 | agent.cbRemain -= channel->header.size; 148 | agent.cbSize += channel->header.size; 149 | 150 | if (!agent.cbRemain) 151 | agent_onClipboard(); 152 | 153 | return PS_STATUS_OK; 154 | } 155 | 156 | 157 | uint8_t * data = channel->buffer; 158 | unsigned int dataSize = channel->header.size; 159 | VDAgentMessage * msg = (VDAgentMessage *)data; 160 | data += sizeof(*msg); 161 | dataSize -= sizeof(*msg); 162 | 163 | if (msg->protocol != VD_AGENT_PROTOCOL) 164 | { 165 | PS_LOG_ERROR("VDAgent protocol %d expected, but got %d", 166 | VD_AGENT_PROTOCOL, msg->protocol); 167 | return PS_STATUS_ERROR; 168 | } 169 | 170 | switch(msg->type) 171 | { 172 | case VD_AGENT_ANNOUNCE_CAPABILITIES: 173 | { 174 | VDAgentAnnounceCapabilities * caps = (VDAgentAnnounceCapabilities *)data; 175 | const int capsSize = VD_AGENT_CAPS_SIZE_FROM_MSG_SIZE(msg->size); 176 | 177 | agent.cbSupported = 178 | VD_AGENT_HAS_CAPABILITY(caps->caps, capsSize, 179 | VD_AGENT_CAP_CLIPBOARD_BY_DEMAND) || 180 | VD_AGENT_HAS_CAPABILITY(caps->caps, capsSize, 181 | VD_AGENT_CAP_CLIPBOARD_SELECTION); 182 | 183 | agent.cbSelection = 184 | VD_AGENT_HAS_CAPABILITY(caps->caps, capsSize, 185 | VD_AGENT_CAP_CLIPBOARD_SELECTION); 186 | 187 | if (caps->request) 188 | return agent_sendCaps(false); 189 | 190 | return PS_STATUS_OK; 191 | } 192 | 193 | case VD_AGENT_CLIPBOARD: 194 | case VD_AGENT_CLIPBOARD_REQUEST: 195 | case VD_AGENT_CLIPBOARD_GRAB: 196 | case VD_AGENT_CLIPBOARD_RELEASE: 197 | { 198 | // all clipboard messages might have this 199 | if (agent.cbSelection) 200 | { 201 | struct Selection * selection = (struct Selection *)data; 202 | data += sizeof(*selection); 203 | dataSize -= sizeof(*selection); 204 | } 205 | 206 | switch(msg->type) 207 | { 208 | case VD_AGENT_CLIPBOARD_RELEASE: 209 | agent.cbAgentGrabbed = false; 210 | if (g_ps.config.clipboard.enable) 211 | g_ps.config.clipboard.release(); 212 | return PS_STATUS_OK; 213 | 214 | case VD_AGENT_CLIPBOARD: 215 | { 216 | uint32_t * type = (uint32_t *)data; 217 | data += sizeof(*type); 218 | dataSize -= sizeof(*type); 219 | 220 | if (agent.cbBuffer) 221 | { 222 | PS_LOG_ERROR( 223 | "Agent tried to send a new clipboard instead of remaining data"); 224 | return PS_STATUS_ERROR; 225 | } 226 | 227 | const unsigned int totalData = msg->size - sizeof(*type); 228 | agent.cbBuffer = (uint8_t *)malloc(totalData); 229 | if (!agent.cbBuffer) 230 | { 231 | PS_LOG_ERROR("Failed to allocate buffer for clipboard transfer"); 232 | return PS_STATUS_ERROR; 233 | } 234 | 235 | agent.cbSize = dataSize; 236 | agent.cbRemain = totalData - dataSize; 237 | memcpy(agent.cbBuffer, data, dataSize); 238 | 239 | if (agent.cbRemain == 0) 240 | agent_onClipboard(); 241 | 242 | return PS_STATUS_OK; 243 | } 244 | 245 | case VD_AGENT_CLIPBOARD_REQUEST: 246 | { 247 | uint32_t * type = (uint32_t *)data; 248 | data += sizeof(type); 249 | 250 | if (g_ps.config.clipboard.enable) 251 | g_ps.config.clipboard.request(agentTypeToPSType(*type)); 252 | return PS_STATUS_OK; 253 | } 254 | 255 | case VD_AGENT_CLIPBOARD_GRAB: 256 | { 257 | uint32_t *types = (uint32_t *)data; 258 | data += sizeof(*types); 259 | 260 | // there is zero documentation on the types field, it might be a 261 | // bitfield but for now we are going to assume it's not. 262 | 263 | agent.cbType = agentTypeToPSType(types[0]); 264 | agent.cbAgentGrabbed = true; 265 | agent.cbClientGrabbed = false; 266 | if (agent.cbSelection) 267 | { 268 | // Windows doesnt support this, so until it's needed there is no point 269 | // messing with it 270 | return PS_STATUS_OK; 271 | } 272 | 273 | if (g_ps.config.clipboard.enable) 274 | g_ps.config.clipboard.notice(agent.cbType); 275 | 276 | return PS_STATUS_OK; 277 | } 278 | } 279 | } 280 | } 281 | 282 | return PS_STATUS_OK; 283 | } 284 | 285 | static void agent_onClipboard(void) 286 | { 287 | if (g_ps.config.clipboard.enable) 288 | g_ps.config.clipboard.data(agent.cbType, agent.cbBuffer, agent.cbSize); 289 | 290 | free(agent.cbBuffer); 291 | agent.cbBuffer = NULL; 292 | agent.cbSize = 0; 293 | agent.cbRemain = 0; 294 | } 295 | 296 | void agent_setServerTokens(unsigned int tokens) 297 | { 298 | atomic_store(&agent.serverTokens, tokens); 299 | } 300 | 301 | static bool agent_takeServerToken(void) 302 | { 303 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_MAIN]; 304 | 305 | unsigned int tokens; 306 | do 307 | { 308 | if (!channel->connected) 309 | return false; 310 | 311 | tokens = atomic_load(&agent.serverTokens); 312 | if (tokens == 0) 313 | return false; 314 | } 315 | while(!atomic_compare_exchange_weak(&agent.serverTokens, &tokens, tokens - 1)); 316 | 317 | return true; 318 | } 319 | 320 | void agent_returnServerTokens(unsigned int tokens) 321 | { 322 | atomic_fetch_add(&agent.serverTokens, tokens); 323 | } 324 | 325 | bool agent_processQueue(void) 326 | { 327 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_MAIN]; 328 | 329 | SPICE_LOCK(channel->lock); 330 | while (queue_peek(agent.queue, NULL) && agent_takeServerToken()) 331 | { 332 | void * msg; 333 | queue_shift(agent.queue, &msg); 334 | if (!SPICE_SEND_PACKET_NL(channel, msg)) 335 | { 336 | SPICE_RAW_PACKET_FREE(msg); 337 | SPICE_UNLOCK(channel->lock); 338 | PS_LOG_ERROR("Failed to send a queued packet"); 339 | return false; 340 | } 341 | SPICE_RAW_PACKET_FREE(msg); 342 | } 343 | SPICE_UNLOCK(channel->lock); 344 | return true; 345 | } 346 | 347 | static bool agent_startMsg(uint32_t type, ssize_t size) 348 | { 349 | VDAgentMessage * msg = 350 | SPICE_PACKET_MALLOC(SPICE_MSGC_MAIN_AGENT_DATA, VDAgentMessage, 0); 351 | 352 | msg->protocol = VD_AGENT_PROTOCOL; 353 | msg->type = type; 354 | msg->opaque = 0; 355 | msg->size = size; 356 | agent.msgSize = size; 357 | queue_push(agent.queue, msg); 358 | 359 | return agent_processQueue(); 360 | } 361 | 362 | static bool agent_writeMsg(const void * buffer_, ssize_t size) 363 | { 364 | assert(size <= agent.msgSize); 365 | 366 | const char * buffer = buffer_; 367 | while(size) 368 | { 369 | const ssize_t toWrite = size > VD_AGENT_MAX_DATA_SIZE ? 370 | VD_AGENT_MAX_DATA_SIZE : size; 371 | 372 | void * msg = SPICE_RAW_PACKET_MALLOC(SPICE_MSGC_MAIN_AGENT_DATA, toWrite, 0); 373 | memcpy(msg, buffer, toWrite); 374 | queue_push(agent.queue, msg); 375 | 376 | size -= toWrite; 377 | buffer += toWrite; 378 | agent.msgSize -= toWrite; 379 | } 380 | 381 | return agent_processQueue(); 382 | } 383 | 384 | static PS_STATUS agent_sendCaps(bool request) 385 | { 386 | if (!agent.present) 387 | return PS_STATUS_ERROR; 388 | 389 | const ssize_t capsSize = sizeof(VDAgentAnnounceCapabilities) + 390 | VD_AGENT_CAPS_BYTES; 391 | VDAgentAnnounceCapabilities *caps = 392 | (VDAgentAnnounceCapabilities *)alloca(capsSize); 393 | memset(caps, 0, capsSize); 394 | 395 | if (g_ps.config.clipboard.enable) 396 | { 397 | caps->request = request ? 1 : 0; 398 | VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND); 399 | VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_SELECTION); 400 | } 401 | 402 | if (!agent_startMsg(VD_AGENT_ANNOUNCE_CAPABILITIES, capsSize) || 403 | !agent_writeMsg(caps, capsSize)) 404 | { 405 | PS_LOG_ERROR("Failed to send our agent capabilities"); 406 | return PS_STATUS_ERROR; 407 | } 408 | 409 | return PS_STATUS_OK; 410 | } 411 | 412 | static uint32_t psTypeToAgentType(PSDataType type) 413 | { 414 | switch(type) 415 | { 416 | case SPICE_DATA_TEXT: return VD_AGENT_CLIPBOARD_UTF8_TEXT ; break; 417 | case SPICE_DATA_PNG : return VD_AGENT_CLIPBOARD_IMAGE_PNG ; break; 418 | case SPICE_DATA_BMP : return VD_AGENT_CLIPBOARD_IMAGE_BMP ; break; 419 | case SPICE_DATA_TIFF: return VD_AGENT_CLIPBOARD_IMAGE_TIFF; break; 420 | case SPICE_DATA_JPEG: return VD_AGENT_CLIPBOARD_IMAGE_JPG ; break; 421 | default: 422 | return VD_AGENT_CLIPBOARD_NONE; 423 | } 424 | } 425 | 426 | static PSDataType agentTypeToPSType(uint32_t type) 427 | { 428 | switch(type) 429 | { 430 | case VD_AGENT_CLIPBOARD_UTF8_TEXT : return SPICE_DATA_TEXT; break; 431 | case VD_AGENT_CLIPBOARD_IMAGE_PNG : return SPICE_DATA_PNG ; break; 432 | case VD_AGENT_CLIPBOARD_IMAGE_BMP : return SPICE_DATA_BMP ; break; 433 | case VD_AGENT_CLIPBOARD_IMAGE_TIFF: return SPICE_DATA_TIFF; break; 434 | case VD_AGENT_CLIPBOARD_IMAGE_JPG : return SPICE_DATA_JPEG; break; 435 | default: 436 | return SPICE_DATA_NONE; 437 | } 438 | } 439 | 440 | bool purespice_clipboardRequest(PSDataType type) 441 | { 442 | if (!agent.present) 443 | return false; 444 | 445 | VDAgentClipboardRequest req; 446 | 447 | if (!agent.cbAgentGrabbed) 448 | return false; 449 | 450 | if (type != agent.cbType) 451 | return false; 452 | 453 | req.type = psTypeToAgentType(type); 454 | if (!agent_startMsg(VD_AGENT_CLIPBOARD_REQUEST, sizeof(req)) || 455 | !agent_writeMsg(&req, sizeof(req))) 456 | { 457 | PS_LOG_ERROR("Failed to write VD_AGENT_CLIPBOARD_REQUEST"); 458 | return false; 459 | } 460 | 461 | return true; 462 | } 463 | 464 | bool purespice_clipboardGrab(PSDataType types[], int count) 465 | { 466 | if (!agent.present) 467 | return false; 468 | 469 | if (count == 0) 470 | return false; 471 | 472 | if (agent.cbSelection) 473 | { 474 | struct Msg 475 | { 476 | uint8_t selection; 477 | uint8_t reserved; 478 | uint32_t types[0]; 479 | }; 480 | 481 | const int size = sizeof(struct Msg) + count * sizeof(uint32_t); 482 | struct Msg * msg = alloca(size); 483 | msg->selection = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; 484 | msg->reserved = 0; 485 | for(int i = 0; i < count; ++i) 486 | msg->types[i] = psTypeToAgentType(types[i]); 487 | 488 | if (!agent_startMsg(VD_AGENT_CLIPBOARD_GRAB, size) || 489 | !agent_writeMsg(msg, size)) 490 | { 491 | PS_LOG_ERROR("Failed to write VD_AGENT_CLIPBOARD_GRAB"); 492 | return false; 493 | } 494 | 495 | agent.cbClientGrabbed = true; 496 | return true; 497 | } 498 | 499 | uint32_t msg[count]; 500 | for(int i = 0; i < count; ++i) 501 | msg[i] = psTypeToAgentType(types[i]); 502 | 503 | if (!agent_startMsg(VD_AGENT_CLIPBOARD_GRAB, sizeof(msg)) || 504 | !agent_writeMsg(&msg, sizeof(msg))) 505 | { 506 | PS_LOG_ERROR("Failed to write VD_AGENT_CLIPBOARD_GRAB"); 507 | return false; 508 | } 509 | 510 | agent.cbClientGrabbed = true; 511 | return true; 512 | } 513 | 514 | bool purespice_clipboardRelease(void) 515 | { 516 | if (!agent.present) 517 | return false; 518 | 519 | // check if if there is anything to release first 520 | if (!agent.cbClientGrabbed) 521 | return true; 522 | 523 | if (agent.cbSelection) 524 | { 525 | uint8_t req[4] = { VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD }; 526 | if (!agent_startMsg(VD_AGENT_CLIPBOARD_RELEASE, sizeof(req)) || 527 | !agent_writeMsg(req, sizeof(req))) 528 | { 529 | PS_LOG_ERROR("Failed to write VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD"); 530 | return false; 531 | } 532 | 533 | agent.cbClientGrabbed = false; 534 | return true; 535 | } 536 | 537 | if (!agent_startMsg(VD_AGENT_CLIPBOARD_RELEASE, 0)) 538 | { 539 | PS_LOG_ERROR("Failed to write VD_AGENT_CLIPBOARD_RELEASE"); 540 | return false; 541 | } 542 | 543 | agent.cbClientGrabbed = false; 544 | return true; 545 | } 546 | 547 | bool purespice_clipboardDataStart(PSDataType type, size_t size) 548 | { 549 | if (!agent.present) 550 | return false; 551 | 552 | uint8_t buffer[8]; 553 | size_t bufSize; 554 | 555 | if (agent.cbSelection) 556 | { 557 | bufSize = 8; 558 | buffer[0] = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; 559 | buffer[1] = buffer[2] = buffer[3] = 0; 560 | ((uint32_t*)buffer)[1] = psTypeToAgentType(type); 561 | } 562 | else 563 | { 564 | bufSize = 4; 565 | ((uint32_t*)buffer)[0] = psTypeToAgentType(type); 566 | } 567 | 568 | if (!agent_startMsg(VD_AGENT_CLIPBOARD, bufSize + size)) 569 | { 570 | PS_LOG_ERROR("Failed to write VD_AGENT_CLIPBOARD start"); 571 | return false; 572 | } 573 | 574 | if (!agent_writeMsg(buffer, bufSize)) 575 | { 576 | PS_LOG_ERROR("Failed to write VD_AGENT_CLIPBOARD data"); 577 | return false; 578 | } 579 | 580 | return true; 581 | } 582 | 583 | bool purespice_clipboardData(PSDataType type, uint8_t * data, size_t size) 584 | { 585 | (void) type; 586 | 587 | if (!agent.present) 588 | return false; 589 | 590 | return agent_writeMsg(data, size); 591 | } 592 | -------------------------------------------------------------------------------- /src/agent.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | bool agent_present(void); 24 | 25 | PS_STATUS agent_connect(void); 26 | 27 | void agent_setServerTokens(unsigned int tokens); 28 | 29 | void agent_returnServerTokens(unsigned int tokens); 30 | 31 | void agent_disconnect(void); 32 | 33 | PS_STATUS agent_process(PSChannel * channel); 34 | 35 | bool agent_processQueue(void); 36 | -------------------------------------------------------------------------------- /src/channel.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | #include "log.h" 23 | #include "channel.h" 24 | #include "locking.h" 25 | #include "messages.h" 26 | #include "rsa.h" 27 | #include "queue.h" 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #include 36 | #include 37 | 38 | static uint64_t get_timestamp(void) 39 | { 40 | struct timespec time; 41 | const int result = clock_gettime(CLOCK_MONOTONIC, &time); 42 | if (result != 0) 43 | perror("clock_gettime failed! this should never happen!\n"); 44 | return (uint64_t)time.tv_sec * 1000LL + time.tv_nsec / 1000000LL; 45 | } 46 | 47 | PS_STATUS channel_connect(PSChannel * channel) 48 | { 49 | PS_STATUS status; 50 | 51 | channel->doDisconnect = false; 52 | channel->initDone = false; 53 | channel->ackFrequency = 0; 54 | channel->ackCount = 0; 55 | 56 | if (channel->spiceType == SPICE_CHANNEL_INPUTS) 57 | SPICE_LOCK_INIT(g_ps.mouse.lock); 58 | 59 | SPICE_LOCK_INIT(channel->lock); 60 | 61 | size_t addrSize; 62 | switch(g_ps.family) 63 | { 64 | case AF_UNIX: 65 | addrSize = sizeof(g_ps.addr.un); 66 | break; 67 | 68 | case AF_INET: 69 | addrSize = sizeof(g_ps.addr.in); 70 | break; 71 | 72 | case AF_INET6: 73 | addrSize = sizeof(g_ps.addr.in6); 74 | break; 75 | 76 | default: 77 | PS_LOG_ERROR("BUG: invalid address family"); 78 | return PS_STATUS_ERROR; 79 | } 80 | 81 | channel->socket = socket(g_ps.family, SOCK_STREAM, 0); 82 | if (channel->socket == -1) 83 | { 84 | PS_LOG_ERROR("Socket creation failed"); 85 | return PS_STATUS_ERROR; 86 | } 87 | 88 | if (g_ps.family != AF_UNIX) 89 | { 90 | const int flag = 1; 91 | (void)setsockopt(channel->socket, IPPROTO_TCP, 92 | TCP_NODELAY , &flag, sizeof(int)); 93 | (void)setsockopt(channel->socket, IPPROTO_TCP, 94 | TCP_QUICKACK, &flag, sizeof(int)); 95 | } 96 | 97 | if (connect(channel->socket, &g_ps.addr.addr, addrSize) == -1) 98 | { 99 | close(channel->socket); 100 | PS_LOG_ERROR("Socket connect failed"); 101 | return PS_STATUS_ERROR; 102 | } 103 | 104 | channel->connected = true; 105 | 106 | const SpiceLinkHeader * p = channel->getConnectPacket(); 107 | if ((size_t)channel_writeNL(channel, p, p->size + sizeof(*p)) != p->size + sizeof(*p)) 108 | { 109 | channel_internal_disconnect(channel); 110 | PS_LOG_ERROR("Failed to write the connect packet"); 111 | return PS_STATUS_ERROR; 112 | } 113 | 114 | SpiceLinkHeader header; 115 | if ((status = channel_readNL(channel, &header, sizeof(header), 116 | NULL)) != PS_STATUS_OK) 117 | { 118 | channel_internal_disconnect(channel); 119 | PS_LOG_ERROR("Failed to read the reply to the connect packet"); 120 | return status; 121 | } 122 | 123 | if (header.magic != SPICE_MAGIC || 124 | header.major_version != SPICE_VERSION_MAJOR) 125 | { 126 | channel_internal_disconnect(channel); 127 | PS_LOG_ERROR("Invalid spice magic and or version"); 128 | return PS_STATUS_ERROR; 129 | } 130 | 131 | if (header.size < sizeof(SpiceLinkReply)) 132 | { 133 | channel_internal_disconnect(channel); 134 | PS_LOG_ERROR("First message < sizeof(SpiceLinkReply)"); 135 | return PS_STATUS_ERROR; 136 | } 137 | 138 | // in practice I have not seen this exceed 186, but it might depending on 139 | // future protocol changes, so put a reaonable upper bound on it 140 | if (header.size > 200) 141 | { 142 | channel_internal_disconnect(channel); 143 | PS_LOG_ERROR("SpiceLinkReply header size seems too large"); 144 | return PS_STATUS_ERROR; 145 | } 146 | 147 | SpiceLinkReply * reply = alloca(header.size); 148 | if ((status = channel_readNL(channel, reply, header.size, 149 | NULL)) != PS_STATUS_OK) 150 | { 151 | channel_internal_disconnect(channel); 152 | return status; 153 | } 154 | 155 | if (reply->error != SPICE_LINK_ERR_OK) 156 | { 157 | channel_internal_disconnect(channel); 158 | PS_LOG_ERROR("Server reported link error: %d", reply->error); 159 | return PS_STATUS_ERROR; 160 | } 161 | 162 | const uint32_t * capsCommon = 163 | (uint32_t *)((uint8_t *)reply + reply->caps_offset); 164 | const uint32_t * capsChannel = 165 | capsCommon + reply->num_common_caps; 166 | 167 | if (channel->setCaps) 168 | channel->setCaps( 169 | capsCommon , reply->num_common_caps, 170 | capsChannel, reply->num_channel_caps); 171 | 172 | SpiceLinkAuthMechanism auth; 173 | auth.auth_mechanism = SPICE_COMMON_CAP_AUTH_SPICE; 174 | if (channel_writeNL(channel, &auth, sizeof(auth)) != sizeof(auth)) 175 | { 176 | channel_internal_disconnect(channel); 177 | PS_LOG_ERROR("Failed to write the auth mechanisim packet"); 178 | return PS_STATUS_ERROR; 179 | } 180 | 181 | PSPassword pass; 182 | if (!rsa_encryptPassword(reply->pub_key, g_ps.config.password, &pass)) 183 | { 184 | channel_internal_disconnect(channel); 185 | PS_LOG_ERROR("Failed to encrypt the password"); 186 | return PS_STATUS_ERROR; 187 | } 188 | 189 | if (channel_writeNL(channel, pass.data, pass.size) != pass.size) 190 | { 191 | rsa_freePassword(&pass); 192 | channel_internal_disconnect(channel); 193 | PS_LOG_ERROR("Failed to write the encrypted password"); 194 | return PS_STATUS_ERROR; 195 | } 196 | 197 | rsa_freePassword(&pass); 198 | 199 | uint32_t linkResult; 200 | if ((status = channel_readNL(channel, &linkResult, sizeof(linkResult), 201 | NULL)) != PS_STATUS_OK) 202 | { 203 | channel_internal_disconnect(channel); 204 | PS_LOG_ERROR("Failed to read the authentication response"); 205 | return status; 206 | } 207 | 208 | if (linkResult != SPICE_LINK_ERR_OK) 209 | { 210 | channel_internal_disconnect(channel); 211 | PS_LOG_ERROR("Server reported link error: %u", linkResult); 212 | return PS_STATUS_ERROR; 213 | } 214 | 215 | struct epoll_event ev = 216 | { 217 | .events = EPOLLIN, 218 | .data.ptr = channel 219 | }; 220 | epoll_ctl(g_ps.epollfd, EPOLL_CTL_ADD, channel->socket, &ev); 221 | 222 | channel->ready = true; 223 | return PS_STATUS_OK; 224 | } 225 | 226 | void channel_internal_disconnect(PSChannel * channel) 227 | { 228 | if (!channel->connected) 229 | return; 230 | 231 | if (channel->ready) 232 | { 233 | channel->ready = false; 234 | 235 | /* disable nodelay so we can trigger a flush after this message */ 236 | int flag; 237 | if (g_ps.family != AF_UNIX) 238 | { 239 | flag = 0; 240 | (void)setsockopt(channel->socket, IPPROTO_TCP, 241 | TCP_NODELAY, (char *)&flag, sizeof(int)); 242 | } 243 | 244 | SpiceMsgcDisconnecting * packet = SPICE_PACKET(SPICE_MSGC_DISCONNECTING, 245 | SpiceMsgcDisconnecting, 0); 246 | packet->time_stamp = get_timestamp(); 247 | packet->reason = SPICE_LINK_ERR_OK; 248 | SPICE_SEND_PACKET(channel, packet); 249 | 250 | /* re-enable nodelay as this triggers a flush according to the man page */ 251 | if (g_ps.family != AF_UNIX) 252 | { 253 | flag = 1; 254 | (void)setsockopt(channel->socket, IPPROTO_TCP, 255 | TCP_NODELAY, (char *)&flag, sizeof(int)); 256 | } 257 | } 258 | 259 | epoll_ctl(g_ps.epollfd, EPOLL_CTL_DEL, channel->socket, NULL); 260 | shutdown(channel->socket, SHUT_WR); 261 | 262 | channel->bufferRead = 0; 263 | channel->headerRead = 0; 264 | channel->bufferSize = 0; 265 | free(channel->buffer); 266 | channel->buffer = NULL; 267 | channel->connected = false; 268 | channel->doDisconnect = false; 269 | 270 | PS_LOG_INFO("%s channel disconnected", channel->name); 271 | } 272 | 273 | void channel_disconnect(PSChannel * channel) 274 | { 275 | if (!channel->connected) 276 | return; 277 | 278 | channel->doDisconnect = true; 279 | } 280 | 281 | static PS_STATUS onMessage_setAck(PSChannel * channel) 282 | { 283 | SpiceMsgSetAck * msg = (SpiceMsgSetAck *)channel->buffer; 284 | 285 | channel->ackFrequency = msg->window; 286 | 287 | SpiceMsgcAckSync * out = 288 | SPICE_PACKET(SPICE_MSGC_ACK_SYNC, SpiceMsgcAckSync, 0); 289 | 290 | out->generation = msg->generation; 291 | return SPICE_SEND_PACKET(channel, out) ? 292 | PS_STATUS_OK : PS_STATUS_ERROR; 293 | } 294 | 295 | static PS_STATUS onMessage_ping(PSChannel * channel) 296 | { 297 | SpiceMsgPing * msg = (SpiceMsgPing *)channel->buffer; 298 | 299 | SpiceMsgcPong * out = 300 | SPICE_PACKET(SPICE_MSGC_PONG, SpiceMsgcPong, 0); 301 | 302 | out->id = msg->id; 303 | out->timestamp = msg->timestamp; 304 | if (!SPICE_SEND_PACKET(channel, out)) 305 | { 306 | PS_LOG_ERROR("Failed to send SpiceMsgcPong"); 307 | return PS_STATUS_ERROR; 308 | } 309 | 310 | return PS_STATUS_OK; 311 | } 312 | 313 | static PS_STATUS onMessage_disconnecting(PSChannel * channel) 314 | { 315 | shutdown(channel->socket, SHUT_WR); 316 | PS_LOG_INFO("Server sent disconnect message"); 317 | return PS_STATUS_HANDLED; 318 | } 319 | 320 | static PS_STATUS onMessage_notify(PSChannel * channel) 321 | { 322 | SpiceMsgNotify * msg = (SpiceMsgNotify *)channel->buffer; 323 | 324 | PS_LOG_INFO("[notify] %s", msg->message); 325 | return PS_STATUS_OK; 326 | } 327 | 328 | PSHandlerFn channel_onMessage(PSChannel * channel) 329 | { 330 | switch(channel->header.type) 331 | { 332 | case SPICE_MSG_MIGRATE: 333 | case SPICE_MSG_MIGRATE_DATA: 334 | return PS_HANDLER_DISCARD; 335 | 336 | case SPICE_MSG_SET_ACK: 337 | return onMessage_setAck; 338 | 339 | case SPICE_MSG_PING: 340 | return onMessage_ping; 341 | 342 | case SPICE_MSG_WAIT_FOR_CHANNELS: 343 | return PS_HANDLER_DISCARD; 344 | 345 | case SPICE_MSG_DISCONNECTING: 346 | return onMessage_disconnecting; 347 | 348 | case SPICE_MSG_NOTIFY: 349 | return onMessage_notify; 350 | } 351 | 352 | return PS_HANDLER_ERROR; 353 | } 354 | 355 | bool channel_ack(PSChannel * channel) 356 | { 357 | if (channel->ackFrequency == 0) 358 | return true; 359 | 360 | if (++channel->ackCount != channel->ackFrequency) 361 | return true; 362 | 363 | channel->ackCount = 0; 364 | 365 | char * ack = SPICE_PACKET(SPICE_MSGC_ACK, char, 0); 366 | *ack = 0; 367 | if (!SPICE_SEND_PACKET(channel, ack)) 368 | { 369 | PS_LOG_ERROR("Failed to write ack packet"); 370 | return false; 371 | } 372 | 373 | return true; 374 | } 375 | 376 | ssize_t channel_writeNL(const PSChannel * channel, 377 | const void * buffer, size_t size) 378 | { 379 | if (!channel->connected) 380 | return -1; 381 | 382 | if (!buffer) 383 | return -1; 384 | 385 | return send(channel->socket, buffer, size, 0); 386 | } 387 | 388 | PS_STATUS channel_readNL(PSChannel * channel, void * buffer, 389 | size_t size, int * dataAvailable) 390 | { 391 | if (!channel->connected) 392 | { 393 | PS_LOG_ERROR("BUG: attempted to read from a closed channel"); 394 | return PS_STATUS_ERROR; 395 | } 396 | 397 | if (!buffer) 398 | { 399 | PS_LOG_ERROR("BUG: attempted to read into a NULL buffer"); 400 | return PS_STATUS_ERROR; 401 | } 402 | 403 | size_t left = size; 404 | uint8_t * buf = (uint8_t *)buffer; 405 | while(left) 406 | { 407 | ssize_t len = read(channel->socket, buf, left); 408 | if (len == 0) 409 | return PS_STATUS_NODATA; 410 | 411 | if (len < 0) 412 | { 413 | channel->connected = false; 414 | PS_LOG_ERROR("Failed to read from the socket: %ld", len); 415 | return PS_STATUS_ERROR; 416 | } 417 | left -= len; 418 | buf += len; 419 | 420 | if (dataAvailable) 421 | *dataAvailable -= len; 422 | } 423 | 424 | return PS_STATUS_OK; 425 | } 426 | 427 | PS_STATUS channel_discardNL(PSChannel * channel, 428 | size_t size, int * dataAvailable) 429 | { 430 | uint8_t c[1024]; 431 | size_t left = size; 432 | while(left) 433 | { 434 | ssize_t len = read(channel->socket, c, left > sizeof(c) ? sizeof(c) : left); 435 | if (len == 0) 436 | return PS_STATUS_NODATA; 437 | 438 | if (len < 0) 439 | { 440 | channel->connected = false; 441 | PS_LOG_ERROR("Failed to read from the socket: %ld", len); 442 | return PS_STATUS_ERROR; 443 | } 444 | 445 | left -= len; 446 | 447 | if (dataAvailable) 448 | *dataAvailable -= len; 449 | } 450 | 451 | return PS_STATUS_OK; 452 | } 453 | -------------------------------------------------------------------------------- /src/channel.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | PS_STATUS channel_connect(PSChannel * channel); 24 | 25 | void channel_internal_disconnect(PSChannel * channel); 26 | 27 | void channel_disconnect(PSChannel * channel); 28 | 29 | PSHandlerFn channel_onMessage(PSChannel * channel); 30 | 31 | bool channel_ack(PSChannel * channel); 32 | 33 | ssize_t channel_writeNL(const PSChannel * channel, 34 | const void * buffer, size_t size); 35 | 36 | PS_STATUS channel_readNL(PSChannel * channel, void * buffer, 37 | size_t size, int * dataAvailable); 38 | 39 | PS_STATUS channel_discardNL(PSChannel * channel, 40 | size_t size, int * dataAvailable); 41 | -------------------------------------------------------------------------------- /src/channel_cursor.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | 23 | #include "ps.h" 24 | #include "log.h" 25 | #include "channel.h" 26 | #include "channel_cursor.h" 27 | 28 | #include 29 | 30 | #include "messages.h" 31 | 32 | const SpiceLinkHeader * channelCursor_getConnectPacket(void) 33 | { 34 | typedef struct 35 | { 36 | SpiceLinkHeader header; 37 | SpiceLinkMess message; 38 | uint32_t supportCaps[COMMON_CAPS_BYTES / sizeof(uint32_t)]; 39 | uint32_t channelCaps[RECORD_CAPS_BYTES / sizeof(uint32_t)]; 40 | } 41 | __attribute__((packed)) ConnectPacket; 42 | 43 | static ConnectPacket p = 44 | { 45 | .header = { 46 | .magic = SPICE_MAGIC , 47 | .major_version = SPICE_VERSION_MAJOR, 48 | .minor_version = SPICE_VERSION_MINOR, 49 | .size = sizeof(ConnectPacket) - sizeof(SpiceLinkHeader) 50 | }, 51 | .message = { 52 | .channel_type = SPICE_CHANNEL_CURSOR, 53 | .num_common_caps = COMMON_CAPS_BYTES / sizeof(uint32_t), 54 | .num_channel_caps = CURSOR_CAPS_BYTES / sizeof(uint32_t), 55 | .caps_offset = sizeof(SpiceLinkMess) 56 | } 57 | }; 58 | 59 | p.message.connection_id = g_ps.sessionID; 60 | p.message.channel_id = g_ps.channelID; 61 | 62 | memset(p.supportCaps, 0, sizeof(p.supportCaps)); 63 | memset(p.channelCaps, 0, sizeof(p.channelCaps)); 64 | 65 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION); 66 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_AUTH_SPICE ); 67 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_MINI_HEADER ); 68 | 69 | return &p.header; 70 | } 71 | 72 | static size_t cursorBufferSize(SpiceCursorHeader * header) 73 | { 74 | const unsigned width = (unsigned)header->width; 75 | const unsigned height = (unsigned)header->height; 76 | 77 | switch (header->type) 78 | { 79 | case SPICE_CURSOR_TYPE_ALPHA: 80 | return width * height * 4; 81 | 82 | case SPICE_CURSOR_TYPE_MONO: 83 | return (width + 7) / 8 * height * 2; 84 | 85 | case SPICE_CURSOR_TYPE_COLOR4: 86 | return (width + 1) / 2 * height + 16 * sizeof(uint32_t) + 87 | (width + 7) / 8 * height; 88 | 89 | case SPICE_CURSOR_TYPE_COLOR8: 90 | return width * height + 256 * sizeof(uint32_t) + 91 | (width + 7) / 8 * height; 92 | 93 | case SPICE_CURSOR_TYPE_COLOR16: 94 | return width * height * 2 + 95 | (width + 7) / 8 * height; 96 | 97 | case SPICE_CURSOR_TYPE_COLOR24: 98 | return width * height * 3 + 99 | (width + 7) / 8 * height; 100 | 101 | case SPICE_CURSOR_TYPE_COLOR32: 102 | return width * height * 4 + 103 | (width + 7) / 8 * height; 104 | } 105 | 106 | return 0; 107 | } 108 | 109 | static struct PSCursorImage * loadCursor(uint64_t id) 110 | { 111 | for (struct PSCursorImage * node = g_ps.cursor.cache; node; node = node->next) 112 | if (node->header.unique == id) 113 | return node; 114 | 115 | return NULL; 116 | } 117 | 118 | static struct PSCursorImage * convertCursor(SpiceCursor * cursor) 119 | { 120 | if (cursor->flags & SPICE_CURSOR_FLAGS_NONE) 121 | return NULL; 122 | 123 | if (cursor->flags & SPICE_CURSOR_FLAGS_FROM_CACHE) 124 | return loadCursor(cursor->header.unique); 125 | 126 | if (cursor->header.width > 512 || cursor->header.height > 512) 127 | { 128 | PS_LOG_ERROR("Unexpected cursor size: %ux%u", 129 | cursor->header.width, cursor->header.height); 130 | return NULL; 131 | } 132 | 133 | size_t bufferSize = cursorBufferSize(&cursor->header); 134 | struct PSCursorImage * node = malloc(sizeof(struct PSCursorImage) + bufferSize); 135 | 136 | node->cached = cursor->flags & SPICE_CURSOR_FLAGS_CACHE_ME; 137 | memcpy(&node->header, &cursor->header, sizeof(node->header)); 138 | memcpy(node->buffer, cursor->data, bufferSize); 139 | 140 | if (node->cached) 141 | { 142 | node->next = NULL; 143 | *g_ps.cursor.cacheLast = node; 144 | g_ps.cursor.cacheLast = &node->next; 145 | } 146 | 147 | return node; 148 | } 149 | 150 | static void clearCursorCache(void) 151 | { 152 | struct PSCursorImage * node; 153 | struct PSCursorImage * next; 154 | for (node = g_ps.cursor.cache; node; node = next) 155 | { 156 | next = node->next; 157 | free(node); 158 | } 159 | 160 | g_ps.cursor.cache = NULL; 161 | g_ps.cursor.cacheLast = &g_ps.cursor.cache; 162 | } 163 | 164 | static void updateCursorImage(void) 165 | { 166 | if (!g_ps.cursor.current) 167 | return; 168 | 169 | switch (g_ps.cursor.current->header.type) 170 | { 171 | case SPICE_CURSOR_TYPE_ALPHA: 172 | g_ps.config.cursor.setRGBAImage( 173 | g_ps.cursor.current->header.width, 174 | g_ps.cursor.current->header.height, 175 | g_ps.cursor.current->header.hot_spot_x, 176 | g_ps.cursor.current->header.hot_spot_y, 177 | g_ps.cursor.current->buffer 178 | ); 179 | break; 180 | 181 | case SPICE_CURSOR_TYPE_MONO: 182 | { 183 | const unsigned width = g_ps.cursor.current->header.width; 184 | const unsigned height = g_ps.cursor.current->header.height; 185 | const unsigned size = (width + 7) / 8 * height; 186 | 187 | const uint8_t * xorBuffer = g_ps.cursor.current->buffer; 188 | const uint8_t * andBuffer = xorBuffer + size; 189 | 190 | g_ps.config.cursor.setMonoImage( 191 | g_ps.cursor.current->header.width, 192 | g_ps.cursor.current->header.height, 193 | g_ps.cursor.current->header.hot_spot_x, 194 | g_ps.cursor.current->header.hot_spot_y, 195 | xorBuffer, 196 | andBuffer 197 | ); 198 | break; 199 | } 200 | 201 | default: 202 | PS_LOG_ERROR("Attempt to use unsupported cursor type: %d", 203 | g_ps.cursor.current->header.type); 204 | } 205 | } 206 | 207 | static void updateCursorStatus(void) 208 | { 209 | g_ps.config.cursor.setState(g_ps.cursor.visible, g_ps.cursor.x, g_ps.cursor.y); 210 | } 211 | 212 | static void updateCursorTrail(void) 213 | { 214 | if (g_ps.config.cursor.setTrail) 215 | g_ps.config.cursor.setTrail(g_ps.cursor.trailLen, g_ps.cursor.trailFreq); 216 | } 217 | 218 | static PS_STATUS onMessage_cursorInit(PSChannel * channel) 219 | { 220 | SpiceMsgCursorInit * msg = (SpiceMsgCursorInit *)channel->buffer; 221 | 222 | g_ps.cursor.x = msg->position.x; 223 | g_ps.cursor.y = msg->position.y; 224 | g_ps.cursor.visible = msg->visible; 225 | g_ps.cursor.trailLen = msg->trail_length; 226 | g_ps.cursor.trailFreq = msg->trail_frequency; 227 | 228 | g_ps.cursor.cache = NULL; 229 | g_ps.cursor.cacheLast = &g_ps.cursor.cache; 230 | g_ps.cursor.current = convertCursor(&msg->cursor); 231 | 232 | if (!g_ps.cursor.current) 233 | g_ps.cursor.visible = false; 234 | 235 | updateCursorImage(); 236 | updateCursorStatus(); 237 | updateCursorTrail(); 238 | 239 | return PS_STATUS_OK; 240 | } 241 | 242 | static PS_STATUS onMessage_cursorReset(PSChannel * channel) 243 | { 244 | (void) channel; 245 | 246 | g_ps.cursor.visible = false; 247 | g_ps.cursor.current = NULL; 248 | 249 | struct PSCursorImage * node; 250 | struct PSCursorImage * next; 251 | for (node = g_ps.cursor.cache; node; node = next) 252 | { 253 | next = node->next; 254 | free(node); 255 | } 256 | 257 | clearCursorCache(); 258 | 259 | return PS_STATUS_OK; 260 | } 261 | 262 | static PS_STATUS onMessage_cursorSet(PSChannel * channel) 263 | { 264 | SpiceMsgCursorSet * msg = (SpiceMsgCursorSet *)channel->buffer; 265 | 266 | g_ps.cursor.x = msg->position.x; 267 | g_ps.cursor.y = msg->position.y; 268 | g_ps.cursor.visible = msg->visible; 269 | 270 | if (g_ps.cursor.current && !g_ps.cursor.current->cached) 271 | free(g_ps.cursor.current); 272 | 273 | g_ps.cursor.current = convertCursor(&msg->cursor); 274 | 275 | if (!g_ps.cursor.current) 276 | g_ps.cursor.visible = false; 277 | 278 | updateCursorStatus(); 279 | updateCursorImage(); 280 | 281 | return PS_STATUS_OK; 282 | } 283 | 284 | static PS_STATUS onMessage_cursorMove(PSChannel * channel) 285 | { 286 | SpiceMsgCursorMove * msg = (SpiceMsgCursorMove *)channel->buffer; 287 | 288 | g_ps.cursor.x = msg->position.x; 289 | g_ps.cursor.y = msg->position.y; 290 | updateCursorStatus(); 291 | 292 | return PS_STATUS_OK; 293 | } 294 | 295 | static PS_STATUS onMessage_cursorHide(PSChannel * channel) 296 | { 297 | (void) channel; 298 | 299 | g_ps.cursor.visible = false; 300 | updateCursorStatus(); 301 | 302 | return PS_STATUS_OK; 303 | } 304 | 305 | static PS_STATUS onMessage_cursorTrail(PSChannel * channel) 306 | { 307 | SpiceMsgCursorTrail * msg = (SpiceMsgCursorTrail *)channel->buffer; 308 | 309 | g_ps.cursor.trailLen = msg->length; 310 | g_ps.cursor.trailFreq = msg->frequency; 311 | updateCursorTrail(); 312 | 313 | return PS_STATUS_OK; 314 | } 315 | 316 | static PS_STATUS onMessage_cursorInvalOne(PSChannel * channel) 317 | { 318 | SpiceMsgCursorInvalOne * msg = (SpiceMsgCursorInvalOne *)channel->buffer; 319 | 320 | struct PSCursorImage ** prev = &g_ps.cursor.cache; 321 | struct PSCursorImage * node = g_ps.cursor.cache; 322 | 323 | while (node) 324 | { 325 | if (node->header.unique == msg->cursor_id) 326 | { 327 | *prev = node->next; 328 | if (!node->next) 329 | g_ps.cursor.cacheLast = prev; 330 | break; 331 | } 332 | 333 | prev = &node->next; 334 | node = node->next; 335 | } 336 | 337 | return PS_STATUS_OK; 338 | } 339 | 340 | static PS_STATUS onMessage_cursorInvalAll(PSChannel * channel) 341 | { 342 | (void) channel; 343 | 344 | clearCursorCache(); 345 | 346 | return PS_STATUS_OK; 347 | } 348 | 349 | PSHandlerFn channelCursor_onMessage(PSChannel * channel) 350 | { 351 | channel->initDone = true; 352 | switch(channel->header.type) 353 | { 354 | case SPICE_MSG_CURSOR_INIT: 355 | return onMessage_cursorInit; 356 | 357 | case SPICE_MSG_CURSOR_RESET: 358 | return onMessage_cursorReset; 359 | 360 | case SPICE_MSG_CURSOR_SET: 361 | return onMessage_cursorSet; 362 | 363 | case SPICE_MSG_CURSOR_MOVE: 364 | return onMessage_cursorMove; 365 | 366 | case SPICE_MSG_CURSOR_HIDE: 367 | return onMessage_cursorHide; 368 | 369 | case SPICE_MSG_CURSOR_TRAIL: 370 | return onMessage_cursorTrail; 371 | 372 | case SPICE_MSG_CURSOR_INVAL_ONE: 373 | return onMessage_cursorInvalOne; 374 | 375 | case SPICE_MSG_CURSOR_INVAL_ALL: 376 | return onMessage_cursorInvalAll; 377 | } 378 | 379 | return PS_HANDLER_DISCARD; 380 | } 381 | -------------------------------------------------------------------------------- /src/channel_cursor.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | const SpiceLinkHeader * channelCursor_getConnectPacket(void); 24 | 25 | PSHandlerFn channelCursor_onMessage(PSChannel * channel); 26 | -------------------------------------------------------------------------------- /src/channel_display.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | #include 23 | 24 | #include "ps.h" 25 | #include "log.h" 26 | #include "channel.h" 27 | #include "channel_playback.h" 28 | 29 | #include "messages.h" 30 | 31 | const SpiceLinkHeader * channelDisplay_getConnectPacket(void) 32 | { 33 | typedef struct 34 | { 35 | SpiceLinkHeader header; 36 | SpiceLinkMess message; 37 | uint32_t supportCaps[COMMON_CAPS_BYTES / sizeof(uint32_t)]; 38 | uint32_t channelCaps[DISPLAY_CAPS_BYTES / sizeof(uint32_t)]; 39 | } 40 | __attribute__((packed)) ConnectPacket; 41 | 42 | static ConnectPacket p = 43 | { 44 | .header = { 45 | .magic = SPICE_MAGIC , 46 | .major_version = SPICE_VERSION_MAJOR, 47 | .minor_version = SPICE_VERSION_MINOR, 48 | .size = sizeof(ConnectPacket) - sizeof(SpiceLinkHeader) 49 | }, 50 | .message = { 51 | .channel_type = SPICE_CHANNEL_DISPLAY, 52 | .num_common_caps = COMMON_CAPS_BYTES / sizeof(uint32_t), 53 | .num_channel_caps = DISPLAY_CAPS_BYTES / sizeof(uint32_t), 54 | .caps_offset = sizeof(SpiceLinkMess) 55 | } 56 | }; 57 | 58 | p.message.connection_id = g_ps.sessionID; 59 | p.message.channel_id = g_ps.channelID; 60 | 61 | memset(p.supportCaps, 0, sizeof(p.supportCaps)); 62 | memset(p.channelCaps, 0, sizeof(p.channelCaps)); 63 | 64 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION); 65 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_AUTH_SPICE ); 66 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_MINI_HEADER ); 67 | 68 | DISPLAY_SET_CAPABILITY(p.supportCaps, SPICE_DISPLAY_CAP_PREF_COMPRESSION); 69 | 70 | return &p.header; 71 | } 72 | 73 | PS_STATUS channelDisplay_onConnect(PSChannel * channel) 74 | { 75 | { 76 | SpiceMsgcDisplayInit * msg = 77 | SPICE_PACKET(SPICE_MSGC_DISPLAY_INIT, 78 | SpiceMsgcDisplayInit, 0); 79 | 80 | memset(msg, 0, sizeof(*msg)); 81 | if (!SPICE_SEND_PACKET(channel, msg)) 82 | { 83 | PS_LOG_ERROR("Failed to send SpiceMsgcDisplayInit"); 84 | return PS_STATUS_ERROR; 85 | } 86 | } 87 | 88 | { 89 | SpiceMsgcPreferredCompression * msg = 90 | SPICE_PACKET(SPICE_MSGC_DISPLAY_PREFERRED_COMPRESSION, 91 | SpiceMsgcPreferredCompression, 0); 92 | 93 | msg->image_compression = SPICE_IMAGE_COMPRESSION_OFF; 94 | if (!SPICE_SEND_PACKET(channel, msg)) 95 | { 96 | PS_LOG_ERROR("Failed to send SpiceMsgcPreferredCompression"); 97 | return PS_STATUS_ERROR; 98 | } 99 | } 100 | 101 | return PS_STATUS_OK; 102 | } 103 | 104 | static void resolveDisplayBase(uint8_t ** ptr, SpiceMsgDisplayBase * base) 105 | { 106 | memcpy(&base->surface_id, *ptr, sizeof(base->surface_id)); 107 | *ptr += sizeof(base->surface_id); 108 | 109 | memcpy(&base->box, *ptr, sizeof(base->box)); 110 | *ptr += sizeof(base->box); 111 | 112 | memcpy(&base->clip.type, *ptr, sizeof(base->clip.type)); 113 | *ptr += sizeof(base->clip.type); 114 | 115 | if (base->clip.type == SPICE_CLIP_TYPE_RECTS) 116 | { 117 | base->clip.rects = (SpiceClipRects *)*ptr; 118 | *ptr += sizeof(base->clip.rects->num_rects); 119 | *ptr += base->clip.rects->num_rects * sizeof(SpiceRect); 120 | } 121 | } 122 | 123 | static void resolveSpicePoint(uint8_t ** ptr, SpicePoint * dst) 124 | { 125 | memcpy(dst, *ptr, sizeof(*dst)); 126 | *ptr += sizeof(*dst); 127 | } 128 | 129 | static void resolveSpicePalette(const uint8_t * data, uint8_t ** ptr, 130 | SpicePalette ** dst, uint64_t *dst_id) 131 | { 132 | uint32_t offset; 133 | memcpy(&offset, *ptr, sizeof(offset)); 134 | *ptr += sizeof(offset); 135 | 136 | if (offset) 137 | { 138 | *dst = (SpicePalette *)data + offset; 139 | memcpy(dst_id, ptr, sizeof(*dst_id)); 140 | *ptr += sizeof(*dst_id); 141 | } 142 | else 143 | { 144 | *dst = NULL; 145 | *dst_id = 0; 146 | } 147 | } 148 | 149 | static void resolveSpiceImage(const uint8_t * data, uint8_t ** ptr, 150 | SpiceImage ** dst) 151 | { 152 | uint32_t offset; 153 | memcpy(&offset, *ptr, sizeof(offset)); 154 | *ptr += sizeof(offset); 155 | *dst = (SpiceImage *)(offset > 0 ? data + offset : NULL); 156 | } 157 | 158 | static void resolveSpiceQMask(const uint8_t * data, uint8_t **ptr, 159 | SpiceQMask * dst) 160 | { 161 | const int copy = 162 | sizeof(dst->flags) + 163 | sizeof(dst->pos ); 164 | 165 | memcpy(dst, *ptr, copy); 166 | *ptr += copy; 167 | 168 | resolveSpiceImage(data, ptr, &dst->bitmap); 169 | } 170 | 171 | static void resolveSpiceCopy(const uint8_t * data, uint8_t ** ptr, 172 | SpiceCopy * dst) 173 | { 174 | resolveSpiceImage(data, ptr, &dst->src_bitmap); 175 | 176 | memcpy(&dst->meta, *ptr, sizeof(dst->meta)); 177 | *ptr += sizeof(dst->meta); 178 | 179 | resolveSpiceQMask(data, ptr, &dst->mask); 180 | } 181 | 182 | static void resolveSpicePattern(const uint8_t * data, uint8_t **ptr, 183 | SpicePattern * dst) 184 | { 185 | resolveSpiceImage(data, ptr, &dst->pat); 186 | resolveSpicePoint( ptr, &dst->pos); 187 | } 188 | 189 | static void resolveSpiceBrush(const uint8_t * data, uint8_t **ptr, 190 | SpiceBrush * dst) 191 | { 192 | memcpy(&dst->type, *ptr, sizeof(dst->type)); 193 | *ptr += sizeof(dst->type); 194 | 195 | switch(dst->type) 196 | { 197 | case SPICE_BRUSH_TYPE_NONE: 198 | return; 199 | 200 | case SPICE_BRUSH_TYPE_SOLID: 201 | memcpy(&dst->u.color, *ptr, sizeof(dst->u.color)); 202 | *ptr += sizeof(dst->u.color); 203 | return; 204 | 205 | case SPICE_BRUSH_TYPE_PATTERN: 206 | resolveSpicePattern(data, ptr, &dst->u.pattern); 207 | return; 208 | } 209 | } 210 | 211 | static void resolveSpiceFill(const uint8_t * data, uint8_t **ptr, 212 | SpiceFill * dst) 213 | { 214 | resolveSpiceBrush(data, ptr, &dst->brush); 215 | 216 | memcpy(&dst->rop_descriptor, *ptr, sizeof(dst->rop_descriptor)); 217 | *ptr += sizeof(dst->rop_descriptor); 218 | 219 | resolveSpiceQMask(data, ptr, &dst->mask); 220 | } 221 | 222 | static void readSpiceBitmap(const uint8_t * data, const SpiceImage * img, 223 | SpiceBitmap * dst) 224 | { 225 | uint8_t * ptr = (uint8_t *)&img->u.bitmap; 226 | 227 | const int copy = 228 | sizeof(dst->format) + 229 | sizeof(dst->flags ) + 230 | sizeof(dst->x ) + 231 | sizeof(dst->y ) + 232 | sizeof(dst->stride); 233 | 234 | memcpy(dst, ptr, copy); 235 | ptr += copy; 236 | 237 | resolveSpicePalette(data, &ptr, &dst->palette, &dst->palette_id); 238 | dst->data = ptr; 239 | } 240 | 241 | static void resolveDisplayDrawCopy(uint8_t * data, SpiceMsgDisplayDrawCopy * dst) 242 | { 243 | uint8_t * ptr = data; 244 | resolveDisplayBase( &ptr, &dst->base); 245 | resolveSpiceCopy (data, &ptr, &dst->data); 246 | } 247 | 248 | static void resolveDisplayDrawFill(uint8_t * data, SpiceMsgDisplayDrawFill * dst) 249 | { 250 | uint8_t * ptr = data; 251 | resolveDisplayBase( &ptr, &dst->base); 252 | resolveSpiceFill (data, &ptr, &dst->data); 253 | } 254 | 255 | static PS_STATUS onMessage_displaySurfaceCreate(PSChannel * channel) 256 | { 257 | SpiceMsgSurfaceCreate * msg = (SpiceMsgSurfaceCreate *)channel->buffer; 258 | 259 | PSSurfaceFormat fmt; 260 | switch((SpiceSurfaceFmt)msg->format) 261 | { 262 | case SPICE_SURFACE_FMT_1_A : fmt = PS_SURFACE_FMT_1_A ; break; 263 | case SPICE_SURFACE_FMT_8_A : fmt = PS_SURFACE_FMT_8_A ; break; 264 | case SPICE_SURFACE_FMT_16_555 : fmt = PS_SURFACE_FMT_16_555 ; break; 265 | case SPICE_SURFACE_FMT_32_xRGB: fmt = PS_SURFACE_FMT_32_xRGB; break; 266 | case SPICE_SURFACE_FMT_16_565 : fmt = PS_SURFACE_FMT_16_565 ; break; 267 | case SPICE_SURFACE_FMT_32_ARGB: fmt = PS_SURFACE_FMT_32_ARGB; break; 268 | 269 | default: 270 | PS_LOG_ERROR("Unknown surface format: %u", msg->format); 271 | return PS_STATUS_ERROR; 272 | } 273 | 274 | g_ps.config.display.surfaceCreate(msg->surface_id, fmt, 275 | msg->width, msg->height); 276 | 277 | return PS_STATUS_OK; 278 | } 279 | 280 | static PS_STATUS onMessage_displaySurfaceDestroy(PSChannel * channel) 281 | { 282 | SpiceMsgSurfaceDestroy * msg = (SpiceMsgSurfaceDestroy *)channel->buffer; 283 | 284 | g_ps.config.display.surfaceDestroy(msg->surface_id); 285 | return PS_STATUS_OK; 286 | } 287 | 288 | static PS_STATUS onMessage_displayDrawFill(PSChannel * channel) 289 | { 290 | SpiceMsgDisplayDrawFill dst; 291 | resolveDisplayDrawFill(channel->buffer, &dst); 292 | 293 | if (dst.data.brush.type != SPICE_BRUSH_TYPE_SOLID) 294 | { 295 | PS_LOG_WARN("PureSpice only supports solid brushes for now"); 296 | return PS_STATUS_OK; 297 | } 298 | 299 | g_ps.config.display.drawFill( 300 | dst.base.surface_id, 301 | dst.base.box.left, 302 | dst.base.box.top, 303 | dst.base.box.right - dst.base.box.left, 304 | dst.base.box.bottom - dst.base.box.top, 305 | dst.data.brush.u.color); 306 | return PS_STATUS_OK; 307 | } 308 | 309 | static PS_STATUS onMessage_displayDrawCopy(PSChannel * channel) 310 | { 311 | SpiceMsgDisplayDrawCopy dst; 312 | resolveDisplayDrawCopy(channel->buffer, &dst); 313 | 314 | // we only support bitmaps for now 315 | if (!dst.data.src_bitmap) 316 | { 317 | PS_LOG_WARN("PureSpice only supports bitmaps for now"); 318 | return PS_STATUS_OK; 319 | } 320 | 321 | switch(dst.data.src_bitmap->descriptor.type) 322 | { 323 | case SPICE_IMAGE_TYPE_BITMAP: 324 | { 325 | SpiceBitmap bmp; 326 | readSpiceBitmap(channel->buffer, dst.data.src_bitmap, &bmp); 327 | const bool topDown = bmp.flags & SPICE_BITMAP_FLAGS_TOP_DOWN; 328 | g_ps.config.display.drawBitmap( 329 | dst.base.surface_id, 330 | PS_BITMAP_FMT_RGBA, 331 | topDown, 332 | dst.base.box.left, 333 | dst.base.box.top, 334 | bmp.x, 335 | bmp.y, 336 | bmp.stride, 337 | bmp.data); 338 | break; 339 | } 340 | 341 | default: 342 | PS_LOG_ERROR("PureSpice does not support compressed formats yet"); 343 | break; 344 | } 345 | 346 | return PS_STATUS_OK; 347 | } 348 | 349 | PSHandlerFn channelDisplay_onMessage(PSChannel * channel) 350 | { 351 | channel->initDone = true; 352 | switch(channel->header.type) 353 | { 354 | case SPICE_MSG_DISPLAY_SURFACE_CREATE: 355 | return onMessage_displaySurfaceCreate; 356 | 357 | case SPICE_MSG_DISPLAY_SURFACE_DESTROY: 358 | return onMessage_displaySurfaceDestroy; 359 | 360 | case SPICE_MSG_DISPLAY_DRAW_FILL: 361 | return onMessage_displayDrawFill; 362 | 363 | case SPICE_MSG_DISPLAY_DRAW_COPY: 364 | return onMessage_displayDrawCopy; 365 | } 366 | 367 | return PS_HANDLER_DISCARD; 368 | } 369 | -------------------------------------------------------------------------------- /src/channel_display.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | const SpiceLinkHeader * channelDisplay_getConnectPacket(void); 24 | 25 | PS_STATUS channelDisplay_onConnect(PSChannel * channel); 26 | 27 | PSHandlerFn channelDisplay_onMessage(PSChannel * channel); 28 | -------------------------------------------------------------------------------- /src/channel_inputs.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | 23 | #include "ps.h" 24 | #include "log.h" 25 | #include "channel.h" 26 | #include "messages.h" 27 | 28 | #include 29 | 30 | const SpiceLinkHeader * channelInputs_getConnectPacket(void) 31 | { 32 | typedef struct 33 | { 34 | SpiceLinkHeader header; 35 | SpiceLinkMess message; 36 | uint32_t supportCaps[COMMON_CAPS_BYTES / sizeof(uint32_t)]; 37 | uint32_t channelCaps[INPUT_CAPS_BYTES / sizeof(uint32_t)]; 38 | } 39 | __attribute__((packed)) ConnectPacket; 40 | 41 | static ConnectPacket p = 42 | { 43 | .header = { 44 | .magic = SPICE_MAGIC , 45 | .major_version = SPICE_VERSION_MAJOR, 46 | .minor_version = SPICE_VERSION_MINOR, 47 | .size = sizeof(ConnectPacket) - sizeof(SpiceLinkHeader) 48 | }, 49 | .message = { 50 | .channel_type = SPICE_CHANNEL_INPUTS, 51 | .num_common_caps = COMMON_CAPS_BYTES / sizeof(uint32_t), 52 | .num_channel_caps = INPUT_CAPS_BYTES / sizeof(uint32_t), 53 | .caps_offset = sizeof(SpiceLinkMess) 54 | } 55 | }; 56 | 57 | p.message.connection_id = g_ps.sessionID; 58 | p.message.channel_id = g_ps.channelID; 59 | 60 | memset(p.supportCaps, 0, sizeof(p.supportCaps)); 61 | memset(p.channelCaps, 0, sizeof(p.channelCaps)); 62 | 63 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION); 64 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_AUTH_SPICE ); 65 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_MINI_HEADER ); 66 | 67 | return &p.header; 68 | } 69 | 70 | static PS_STATUS onMessage_inputsInit(PSChannel * channel) 71 | { 72 | channel->initDone = true; 73 | //SpiceMsgInputsInit * msg = (SpiceMsgInputsInit *)channel->buffer; 74 | 75 | return PS_STATUS_OK; 76 | } 77 | 78 | static PS_STATUS onMessage_inputsKeyModifiers(PSChannel * channel) 79 | { 80 | SpiceMsgInputsInit * msg = (SpiceMsgInputsInit *)channel->buffer; 81 | g_ps.kb.modifiers = msg->modifiers; 82 | return PS_STATUS_OK; 83 | } 84 | 85 | static PS_STATUS onMessage_inputsMouseMotionAck(PSChannel * channel) 86 | { 87 | (void)channel; 88 | 89 | const int count = atomic_fetch_sub(&g_ps.mouse.sentCount, 90 | SPICE_INPUT_MOTION_ACK_BUNCH); 91 | 92 | if (count < SPICE_INPUT_MOTION_ACK_BUNCH) 93 | { 94 | PS_LOG_ERROR("Server sent an ack for more messages then expected"); 95 | return PS_STATUS_ERROR; 96 | } 97 | 98 | return PS_STATUS_OK; 99 | } 100 | 101 | PSHandlerFn channelInputs_onMessage(PSChannel * channel) 102 | { 103 | if (!channel->initDone) 104 | { 105 | if (channel->header.type == SPICE_MSG_INPUTS_INIT) 106 | return onMessage_inputsInit; 107 | 108 | purespice_disconnect(); 109 | PS_LOG_ERROR("Expected SPICE_MSG_INPUTS_INIT but got %d", channel->header.type); 110 | return PS_HANDLER_ERROR; 111 | } 112 | 113 | switch(channel->header.type) 114 | { 115 | case SPICE_MSG_INPUTS_INIT: 116 | purespice_disconnect(); 117 | PS_LOG_ERROR("Unexpected SPICE_MSG_INPUTS_INIT"); 118 | return PS_HANDLER_ERROR; 119 | 120 | case SPICE_MSG_INPUTS_KEY_MODIFIERS: 121 | return onMessage_inputsKeyModifiers; 122 | 123 | case SPICE_MSG_INPUTS_MOUSE_MOTION_ACK: 124 | return onMessage_inputsMouseMotionAck; 125 | } 126 | 127 | return PS_HANDLER_DISCARD; 128 | } 129 | 130 | bool purespice_keyDown(uint32_t code) 131 | { 132 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_INPUTS]; 133 | if (!channel->connected || !channel->ready) 134 | return false; 135 | 136 | if (code > 0x100) 137 | code = 0xe0 | ((code - 0x100) << 8); 138 | 139 | SpiceMsgcKeyDown * msg = 140 | SPICE_PACKET(SPICE_MSGC_INPUTS_KEY_DOWN, 141 | SpiceMsgcKeyDown, 0); 142 | msg->code = code; 143 | 144 | if (!SPICE_SEND_PACKET(channel, msg)) 145 | { 146 | PS_LOG_ERROR("Failed to send SpiceMsgcKeyDown"); 147 | return false; 148 | } 149 | 150 | return true; 151 | } 152 | 153 | bool purespice_keyUp(uint32_t code) 154 | { 155 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_INPUTS]; 156 | if (!channel->connected || !channel->ready) 157 | return false; 158 | 159 | if (code < 0x100) 160 | code |= 0x80; 161 | else 162 | code = 0x80e0 | ((code - 0x100) << 8); 163 | 164 | SpiceMsgcKeyUp * msg = 165 | SPICE_PACKET(SPICE_MSGC_INPUTS_KEY_UP, 166 | SpiceMsgcKeyUp, 0); 167 | msg->code = code; 168 | 169 | if (!SPICE_SEND_PACKET(channel, msg)) 170 | { 171 | PS_LOG_ERROR("Failed to send SpiceMsgcKeyUp"); 172 | return false; 173 | } 174 | 175 | return true; 176 | } 177 | 178 | bool purespice_keyModifiers(uint32_t modifiers) 179 | { 180 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_INPUTS]; 181 | if (!channel->connected || !channel->ready) 182 | return false; 183 | 184 | SpiceMsgcInputsKeyModifiers * msg = 185 | SPICE_PACKET(SPICE_MSGC_INPUTS_KEY_MODIFIERS, 186 | SpiceMsgcInputsKeyModifiers, 0); 187 | msg->modifiers = modifiers; 188 | 189 | if (!SPICE_SEND_PACKET(channel, msg)) 190 | { 191 | PS_LOG_ERROR("Failed to send SpiceMsgcInputsKeyModifiers"); 192 | return false; 193 | } 194 | 195 | return true; 196 | } 197 | 198 | bool purespice_mouseMode(bool server) 199 | { 200 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_MAIN]; 201 | if (!channel->connected || !channel->ready) 202 | return false; 203 | 204 | SpiceMsgcMainMouseModeRequest * msg = SPICE_PACKET( 205 | SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST, 206 | SpiceMsgcMainMouseModeRequest, 0); 207 | 208 | msg->mouse_mode = server ? SPICE_MOUSE_MODE_SERVER : SPICE_MOUSE_MODE_CLIENT; 209 | 210 | if (!SPICE_SEND_PACKET(channel, msg)) 211 | { 212 | PS_LOG_ERROR("Failed to send SpiceMsgcMainMouseModeRequest"); 213 | return false; 214 | } 215 | 216 | return true; 217 | } 218 | 219 | bool purespice_mousePosition(uint32_t x, uint32_t y) 220 | { 221 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_INPUTS]; 222 | if (!channel->connected || !channel->ready) 223 | return false; 224 | 225 | SpiceMsgcMousePosition * msg = 226 | SPICE_PACKET(SPICE_MSGC_INPUTS_MOUSE_POSITION, SpiceMsgcMousePosition, 0); 227 | 228 | SPICE_LOCK(g_ps.mouse.lock); 229 | msg->display_id = 0; 230 | msg->button_state = g_ps.mouse.buttonState; 231 | msg->x = x; 232 | msg->y = y; 233 | SPICE_UNLOCK(g_ps.mouse.lock); 234 | 235 | atomic_fetch_add(&g_ps.mouse.sentCount, 1); 236 | if (!SPICE_SEND_PACKET(channel, msg)) 237 | { 238 | PS_LOG_ERROR("Failed to send SpiceMsgcMousePosition"); 239 | return false; 240 | } 241 | 242 | return true; 243 | } 244 | 245 | bool purespice_mouseMotion(int32_t x, int32_t y) 246 | { 247 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_INPUTS]; 248 | if (!channel->connected || !channel->ready) 249 | return false; 250 | 251 | /* while the protocol supports movements greater then +-127 the QEMU 252 | * virtio-mouse device does not, so we need to split this up into seperate 253 | * messages. For performance we build this as a single buffer otherwise this 254 | * will be split into multiple packets */ 255 | 256 | const unsigned delta = abs(x) > abs(y) ? abs(x) : abs(y); 257 | const unsigned msgs = (delta + 126) / 127; 258 | 259 | // only one message, so just send it normally 260 | if (msgs == 1) 261 | { 262 | SpiceMsgcMouseMotion * msg = 263 | SPICE_PACKET(SPICE_MSGC_INPUTS_MOUSE_MOTION, SpiceMsgcMouseMotion, 0); 264 | 265 | SPICE_LOCK(g_ps.mouse.lock); 266 | msg->x = x; 267 | msg->y = y; 268 | msg->button_state = g_ps.mouse.buttonState; 269 | SPICE_UNLOCK(g_ps.mouse.lock); 270 | 271 | atomic_fetch_add(&g_ps.mouse.sentCount, 1); 272 | if (!SPICE_SEND_PACKET(channel, msg)) 273 | { 274 | PS_LOG_ERROR("Failed to send SpiceMsgcMouseMotion"); 275 | return false; 276 | } 277 | 278 | return true; 279 | } 280 | 281 | const size_t bufferSize = ( 282 | sizeof(SpiceMiniDataHeader ) + 283 | sizeof(SpiceMsgcMouseMotion) 284 | ) * msgs; 285 | 286 | if (bufferSize > g_ps.motionBufferSize) 287 | { 288 | if (g_ps.motionBuffer) 289 | free(g_ps.motionBuffer); 290 | g_ps.motionBuffer = malloc(bufferSize); 291 | g_ps.motionBufferSize = bufferSize; 292 | } 293 | 294 | uint8_t * buffer = g_ps.motionBuffer; 295 | uint8_t * msg = buffer; 296 | 297 | SPICE_LOCK(g_ps.mouse.lock); 298 | while(x != 0 || y != 0) 299 | { 300 | SpiceMiniDataHeader *h = (SpiceMiniDataHeader *)msg; 301 | SpiceMsgcMouseMotion *m = (SpiceMsgcMouseMotion *)(h + 1); 302 | msg = (uint8_t*)(m + 1); 303 | 304 | h->size = sizeof(SpiceMsgcMouseMotion); 305 | h->type = SPICE_MSGC_INPUTS_MOUSE_MOTION; 306 | 307 | m->x = x > 127 ? 127 : (x < -127 ? -127 : x); 308 | m->y = y > 127 ? 127 : (y < -127 ? -127 : y); 309 | m->button_state = g_ps.mouse.buttonState; 310 | 311 | x -= m->x; 312 | y -= m->y; 313 | } 314 | SPICE_UNLOCK(g_ps.mouse.lock); 315 | 316 | atomic_fetch_add(&g_ps.mouse.sentCount, msgs); 317 | 318 | SPICE_LOCK(channel->lock); 319 | const ssize_t wrote = send(channel->socket, buffer, bufferSize, 0); 320 | SPICE_UNLOCK(channel->lock); 321 | 322 | if ((size_t)wrote != bufferSize) 323 | { 324 | PS_LOG_ERROR("Only wrote %ld of the expected %ld bytes", wrote, bufferSize); 325 | return false; 326 | } 327 | 328 | return true; 329 | } 330 | 331 | bool purespice_mousePress(uint32_t button) 332 | { 333 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_INPUTS]; 334 | if (!channel->connected || !channel->ready) 335 | return false; 336 | 337 | SPICE_LOCK(g_ps.mouse.lock); 338 | switch(button) 339 | { 340 | case SPICE_MOUSE_BUTTON_LEFT : 341 | g_ps.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_LEFT ; break; 342 | case SPICE_MOUSE_BUTTON_MIDDLE : 343 | g_ps.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_MIDDLE ; break; 344 | case SPICE_MOUSE_BUTTON_RIGHT : 345 | g_ps.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_RIGHT ; break; 346 | case _SPICE_MOUSE_BUTTON_SIDE : 347 | g_ps.mouse.buttonState |= _SPICE_MOUSE_BUTTON_MASK_SIDE ; break; 348 | case _SPICE_MOUSE_BUTTON_EXTRA : 349 | g_ps.mouse.buttonState |= _SPICE_MOUSE_BUTTON_MASK_EXTRA ; break; 350 | } 351 | 352 | SpiceMsgcMousePress * msg = 353 | SPICE_PACKET(SPICE_MSGC_INPUTS_MOUSE_PRESS, SpiceMsgcMousePress, 0); 354 | 355 | msg->button = button; 356 | msg->button_state = g_ps.mouse.buttonState; 357 | SPICE_UNLOCK(g_ps.mouse.lock); 358 | 359 | if (!SPICE_SEND_PACKET(channel, msg)) 360 | { 361 | PS_LOG_ERROR("Failed to write SpiceMsgcMousePress"); 362 | return false; 363 | } 364 | 365 | return true; 366 | } 367 | 368 | bool purespice_mouseRelease(uint32_t button) 369 | { 370 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_INPUTS]; 371 | if (!channel->connected || !channel->ready) 372 | return false; 373 | 374 | SPICE_LOCK(g_ps.mouse.lock); 375 | switch(button) 376 | { 377 | case SPICE_MOUSE_BUTTON_LEFT : 378 | g_ps.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_LEFT ; break; 379 | case SPICE_MOUSE_BUTTON_MIDDLE : 380 | g_ps.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_MIDDLE ; break; 381 | case SPICE_MOUSE_BUTTON_RIGHT : 382 | g_ps.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_RIGHT ; break; 383 | case _SPICE_MOUSE_BUTTON_SIDE : 384 | g_ps.mouse.buttonState &= ~_SPICE_MOUSE_BUTTON_MASK_SIDE ; break; 385 | case _SPICE_MOUSE_BUTTON_EXTRA : 386 | g_ps.mouse.buttonState &= ~_SPICE_MOUSE_BUTTON_MASK_EXTRA ; break; 387 | } 388 | 389 | SpiceMsgcMouseRelease * msg = 390 | SPICE_PACKET(SPICE_MSGC_INPUTS_MOUSE_RELEASE, SpiceMsgcMouseRelease, 0); 391 | 392 | msg->button = button; 393 | msg->button_state = g_ps.mouse.buttonState; 394 | SPICE_UNLOCK(g_ps.mouse.lock); 395 | 396 | if (!SPICE_SEND_PACKET(channel, msg)) 397 | { 398 | PS_LOG_ERROR("Failed to write SpiceMsgcMouseRelease"); 399 | return false; 400 | } 401 | 402 | return true; 403 | } 404 | -------------------------------------------------------------------------------- /src/channel_inputs.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | const SpiceLinkHeader * channelInputs_getConnectPacket(void); 24 | 25 | PSHandlerFn channelInputs_onMessage(PSChannel * channel); 26 | -------------------------------------------------------------------------------- /src/channel_main.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "log.h" 22 | #include "channel_main.h" 23 | #include "channel.h" 24 | #include "agent.h" 25 | #include "messages.h" 26 | 27 | #include 28 | 29 | struct ChannelMain 30 | { 31 | bool ready; 32 | 33 | bool capAgentTokens; 34 | bool capNameAndUUID; 35 | bool hasName; 36 | bool hasUUID; 37 | bool hasList; 38 | }; 39 | 40 | static struct ChannelMain cm = { 0 }; 41 | 42 | const SpiceLinkHeader * channelMain_getConnectPacket(void) 43 | { 44 | typedef struct 45 | { 46 | SpiceLinkHeader header; 47 | SpiceLinkMess message; 48 | uint32_t supportCaps[COMMON_CAPS_BYTES / sizeof(uint32_t)]; 49 | uint32_t channelCaps[MAIN_CAPS_BYTES / sizeof(uint32_t)]; 50 | } 51 | __attribute__((packed)) ConnectPacket; 52 | 53 | static ConnectPacket p = 54 | { 55 | .header = { 56 | .magic = SPICE_MAGIC , 57 | .major_version = SPICE_VERSION_MAJOR, 58 | .minor_version = SPICE_VERSION_MINOR, 59 | .size = sizeof(ConnectPacket) - sizeof(SpiceLinkHeader) 60 | }, 61 | .message = { 62 | .channel_type = SPICE_CHANNEL_MAIN, 63 | .num_common_caps = COMMON_CAPS_BYTES / sizeof(uint32_t), 64 | .num_channel_caps = MAIN_CAPS_BYTES / sizeof(uint32_t), 65 | .caps_offset = sizeof(SpiceLinkMess) 66 | } 67 | }; 68 | 69 | p.message.connection_id = g_ps.sessionID; 70 | p.message.channel_id = g_ps.channelID; 71 | 72 | memset(p.supportCaps, 0, sizeof(p.supportCaps)); 73 | memset(p.channelCaps, 0, sizeof(p.channelCaps)); 74 | 75 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION); 76 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_AUTH_SPICE ); 77 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_MINI_HEADER ); 78 | 79 | MAIN_SET_CAPABILITY(p.channelCaps, SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS); 80 | MAIN_SET_CAPABILITY(p.channelCaps, SPICE_MAIN_CAP_NAME_AND_UUID ); 81 | 82 | memset(&cm, 0, sizeof(cm)); 83 | 84 | return &p.header; 85 | } 86 | 87 | void channelMain_setCaps(const uint32_t * common, int numCommon, 88 | const uint32_t * channel, int numChannel) 89 | { 90 | /* for whatever reason the spice server does not report that it supports these 91 | * capabilities so we are just going to assume it does until the below PR is 92 | * merged, or indefiniately if it's rejected. 93 | * https://gitlab.freedesktop.org/spice/spice/-/merge_requests/198 94 | */ 95 | #if 0 96 | cm.capAgentTokens = HAS_CAPABILITY(channel, numChannel, 97 | SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS); 98 | cm.capNameAndUUID = HAS_CAPABILITY(channel, numChannel, 99 | SPICE_MAIN_CAP_NAME_AND_UUID); 100 | #else 101 | (void) common; 102 | (void) numCommon; 103 | (void) channel; 104 | (void) numChannel; 105 | cm.capAgentTokens = true; 106 | cm.capNameAndUUID = true; 107 | #endif 108 | } 109 | 110 | static void checkReady(void) 111 | { 112 | if (cm.ready) 113 | return; 114 | 115 | if (cm.capNameAndUUID) 116 | { 117 | if (!cm.hasName || !cm.hasUUID) 118 | return; 119 | } 120 | 121 | if (!cm.hasList) 122 | return; 123 | 124 | cm.ready = true; 125 | if (g_ps.config.ready) 126 | g_ps.config.ready(); 127 | } 128 | 129 | static PS_STATUS onMessage_mainInit(struct PSChannel * channel) 130 | { 131 | channel->initDone = true; 132 | 133 | SpiceMsgMainInit * msg = (SpiceMsgMainInit *)channel->buffer; 134 | g_ps.sessionID = msg->session_id; 135 | agent_setServerTokens(msg->agent_tokens); 136 | 137 | if (msg->agent_connected) 138 | { 139 | PS_STATUS status; 140 | if ((status = agent_connect()) != PS_STATUS_OK) 141 | { 142 | purespice_disconnect(); 143 | return status; 144 | } 145 | } 146 | 147 | if (msg->current_mouse_mode != SPICE_MOUSE_MODE_CLIENT && 148 | !purespice_mouseMode(false)) 149 | { 150 | PS_LOG_ERROR("Failed to set the initial mouse mode"); 151 | return PS_STATUS_ERROR; 152 | } 153 | 154 | void * packet = SPICE_RAW_PACKET(SPICE_MSGC_MAIN_ATTACH_CHANNELS, 0, 0); 155 | if (!SPICE_SEND_PACKET(channel, packet)) 156 | { 157 | purespice_disconnect(); 158 | PS_LOG_ERROR("Failed to write SPICE_MSGC_MAIN_ATTACH_CHANNELS"); 159 | return PS_STATUS_ERROR; 160 | } 161 | 162 | return PS_STATUS_OK; 163 | } 164 | 165 | static PS_STATUS onMessage_mainName(struct PSChannel * channel) 166 | { 167 | SpiceMsgMainName * msg = (SpiceMsgMainName *)channel->buffer; 168 | PS_LOG_INFO("Guest name: %s", msg->name); 169 | 170 | if (g_ps.guestName) 171 | free(g_ps.guestName); 172 | 173 | g_ps.guestName = strdup((char *)msg->name); 174 | cm.hasName = true; 175 | 176 | checkReady(); 177 | return PS_STATUS_OK; 178 | } 179 | 180 | static PS_STATUS onMessage_mainUUID(struct PSChannel * channel) 181 | { 182 | SpiceMsgMainUUID * msg = (SpiceMsgMainUUID *)channel->buffer; 183 | 184 | PS_LOG_INFO("Guest UUID: " 185 | "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", 186 | msg->uuid[ 0], msg->uuid[ 1], msg->uuid[ 2], msg->uuid[ 3], msg->uuid[ 4], 187 | msg->uuid[ 5], msg->uuid[ 6], msg->uuid[ 7], msg->uuid[ 8], msg->uuid[ 9], 188 | msg->uuid[10], msg->uuid[11], msg->uuid[12], msg->uuid[13], msg->uuid[14], 189 | msg->uuid[15]); 190 | 191 | memcpy(g_ps.guestUUID, msg->uuid, sizeof(g_ps.guestUUID)); 192 | cm.hasUUID = true; 193 | 194 | checkReady(); 195 | return PS_STATUS_OK; 196 | } 197 | 198 | static PS_STATUS onMessage_mainChannelsList(struct PSChannel * channel) 199 | { 200 | SpiceMainChannelsList * msg = (SpiceMainChannelsList *)channel->buffer; 201 | 202 | for(int n = 0; n < PS_CHANNEL_MAX; ++n) 203 | { 204 | struct PSChannel * ch = &g_ps.channels[n]; 205 | ch->available = false; 206 | } 207 | 208 | for(size_t i = 0; i < msg->num_of_channels; ++i) 209 | for(int n = 0; n < PS_CHANNEL_MAX; ++n) 210 | { 211 | struct PSChannel * ch = &g_ps.channels[n]; 212 | if (ch->spiceType != msg->channels[i].type) 213 | continue; 214 | 215 | ch->available = true; 216 | if ((ch->enable && !*ch->enable) || (ch->autoConnect && !*ch->autoConnect)) 217 | continue; 218 | 219 | if (ch->connected) 220 | { 221 | purespice_disconnect(); 222 | PS_LOG_ERROR("Protocol error. The server asked us to reconnect an " 223 | "already connected channel (%s)", ch->name); 224 | return PS_STATUS_ERROR; 225 | } 226 | 227 | PS_STATUS status = ps_connectChannel(ch); 228 | if (status != PS_STATUS_OK) 229 | { 230 | purespice_disconnect(); 231 | PS_LOG_ERROR("Failed to connect to the %s channel", ch->name); 232 | return status; 233 | } 234 | 235 | break; 236 | } 237 | 238 | cm.hasList = true; 239 | checkReady(); 240 | return PS_STATUS_OK; 241 | } 242 | 243 | static PS_STATUS onMessage_mainAgentConnected(struct PSChannel * channel) 244 | { 245 | (void)channel; 246 | 247 | PS_STATUS status; 248 | if ((status = agent_connect()) != PS_STATUS_OK) 249 | { 250 | purespice_disconnect(); 251 | return status; 252 | } 253 | 254 | return PS_STATUS_OK; 255 | } 256 | 257 | static PS_STATUS onMessage_mainAgentConnectedTokens(struct PSChannel * channel) 258 | { 259 | uint32_t num_tokens = *(uint32_t *)channel->buffer; 260 | 261 | agent_setServerTokens(num_tokens); 262 | return onMessage_mainAgentConnected(channel); 263 | } 264 | 265 | static PS_STATUS onMessage_mainAgentDisconnected(struct PSChannel * channel) 266 | { 267 | uint32_t error = *(uint32_t *)channel->buffer; 268 | 269 | agent_disconnect(); 270 | PS_LOG_WARN("Disconnected from the spice guest agent: %u", error); 271 | return PS_STATUS_OK; 272 | } 273 | 274 | static PS_STATUS onMessage_mainAgentData(struct PSChannel * channel) 275 | { 276 | PS_STATUS status; 277 | if ((status = agent_process(channel)) != PS_STATUS_OK) 278 | { 279 | PS_LOG_ERROR("Failed to process agent data"); 280 | purespice_disconnect(); 281 | } 282 | 283 | return status; 284 | } 285 | 286 | static PS_STATUS onMessage_mainAgentToken(struct PSChannel * channel) 287 | { 288 | uint32_t num_tokens = *(uint32_t *)channel->buffer; 289 | 290 | agent_returnServerTokens(num_tokens); 291 | if (!agent_processQueue()) 292 | { 293 | purespice_disconnect(); 294 | PS_LOG_ERROR("Failed to process the agent queue"); 295 | return PS_STATUS_ERROR; 296 | } 297 | 298 | return PS_STATUS_OK; 299 | } 300 | 301 | PSHandlerFn channelMain_onMessage(struct PSChannel * channel) 302 | { 303 | if (!channel->initDone) 304 | { 305 | if (channel->header.type == SPICE_MSG_MAIN_INIT) 306 | return onMessage_mainInit; 307 | 308 | purespice_disconnect(); 309 | PS_LOG_ERROR("Expected SPICE_MSG_MAIN_INIT but got %d", channel->header.type); 310 | return PS_HANDLER_ERROR; 311 | } 312 | 313 | switch(channel->header.type) 314 | { 315 | case SPICE_MSG_MAIN_INIT: 316 | purespice_disconnect(); 317 | PS_LOG_ERROR("Unexpected SPICE_MSG_MAIN_INIT"); 318 | return PS_HANDLER_ERROR; 319 | 320 | case SPICE_MSG_MAIN_NAME: 321 | return onMessage_mainName; 322 | 323 | case SPICE_MSG_MAIN_UUID: 324 | return onMessage_mainUUID; 325 | 326 | case SPICE_MSG_MAIN_CHANNELS_LIST: 327 | return onMessage_mainChannelsList; 328 | 329 | case SPICE_MSG_MAIN_MOUSE_MODE: 330 | return PS_HANDLER_DISCARD; 331 | 332 | case SPICE_MSG_MAIN_MULTI_MEDIA_TIME: 333 | return PS_HANDLER_DISCARD; 334 | 335 | case SPICE_MSG_MAIN_AGENT_CONNECTED: 336 | return onMessage_mainAgentConnected; 337 | 338 | case SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS: 339 | return onMessage_mainAgentConnectedTokens; 340 | 341 | case SPICE_MSG_MAIN_AGENT_DISCONNECTED: 342 | return onMessage_mainAgentDisconnected; 343 | 344 | case SPICE_MSG_MAIN_AGENT_DATA: 345 | if (!agent_present()) 346 | return PS_HANDLER_DISCARD; 347 | return onMessage_mainAgentData; 348 | 349 | case SPICE_MSG_MAIN_AGENT_TOKEN: 350 | return onMessage_mainAgentToken; 351 | } 352 | 353 | return PS_HANDLER_ERROR; 354 | } 355 | -------------------------------------------------------------------------------- /src/channel_main.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | const SpiceLinkHeader * channelMain_getConnectPacket(void); 24 | void channelMain_setCaps(const uint32_t * common, int numCommon, 25 | const uint32_t * channel, int numChannel); 26 | PSHandlerFn channelMain_onMessage(struct PSChannel * channel); 27 | -------------------------------------------------------------------------------- /src/channel_playback.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | 23 | #include "ps.h" 24 | #include "log.h" 25 | #include "channel.h" 26 | #include "channel_playback.h" 27 | 28 | #include "messages.h" 29 | 30 | const SpiceLinkHeader * channelPlayback_getConnectPacket(void) 31 | { 32 | typedef struct 33 | { 34 | SpiceLinkHeader header; 35 | SpiceLinkMess message; 36 | uint32_t supportCaps[COMMON_CAPS_BYTES / sizeof(uint32_t)]; 37 | uint32_t channelCaps[PLAYBACK_CAPS_BYTES / sizeof(uint32_t)]; 38 | } 39 | __attribute__((packed)) ConnectPacket; 40 | 41 | static ConnectPacket p = 42 | { 43 | .header = { 44 | .magic = SPICE_MAGIC , 45 | .major_version = SPICE_VERSION_MAJOR, 46 | .minor_version = SPICE_VERSION_MINOR, 47 | .size = sizeof(ConnectPacket) - sizeof(SpiceLinkHeader) 48 | }, 49 | .message = { 50 | .channel_type = SPICE_CHANNEL_PLAYBACK, 51 | .num_common_caps = COMMON_CAPS_BYTES / sizeof(uint32_t), 52 | .num_channel_caps = PLAYBACK_CAPS_BYTES / sizeof(uint32_t), 53 | .caps_offset = sizeof(SpiceLinkMess) 54 | } 55 | }; 56 | 57 | p.message.connection_id = g_ps.sessionID; 58 | p.message.channel_id = g_ps.channelID; 59 | 60 | memset(p.supportCaps, 0, sizeof(p.supportCaps)); 61 | memset(p.channelCaps, 0, sizeof(p.channelCaps)); 62 | 63 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION); 64 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_AUTH_SPICE ); 65 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_MINI_HEADER ); 66 | 67 | if (g_ps.config.playback.volume || g_ps.config.playback.mute) 68 | PLAYBACK_SET_CAPABILITY(p.channelCaps, SPICE_PLAYBACK_CAP_VOLUME); 69 | 70 | return &p.header; 71 | } 72 | 73 | static PS_STATUS onMessage_playbackStart(PSChannel * channel) 74 | { 75 | SpiceMsgPlaybackStart * msg = (SpiceMsgPlaybackStart *)channel->buffer; 76 | 77 | PSAudioFormat fmt = PS_AUDIO_FMT_INVALID; 78 | if (msg->format == SPICE_AUDIO_FMT_S16) 79 | fmt = PS_AUDIO_FMT_S16; 80 | 81 | g_ps.config.playback.start(msg->channels, msg->frequency, fmt, msg->time); 82 | return PS_STATUS_OK; 83 | } 84 | 85 | static PS_STATUS onMessage_playbackData(PSChannel * channel) 86 | { 87 | SpiceMsgPlaybackPacket * msg = (SpiceMsgPlaybackPacket *)channel->buffer; 88 | 89 | g_ps.config.playback.data(msg->data, channel->header.size - sizeof(*msg)); 90 | return PS_STATUS_OK; 91 | } 92 | 93 | static PS_STATUS onMessage_playbackStop(PSChannel * channel) 94 | { 95 | (void)channel; 96 | g_ps.config.playback.stop(); 97 | return PS_STATUS_OK; 98 | } 99 | 100 | static PS_STATUS onMessage_playbackVolume(PSChannel * channel) 101 | { 102 | SpiceMsgAudioVolume * msg = (SpiceMsgAudioVolume *)channel->buffer; 103 | 104 | uint16_t volume[msg->nchannels]; 105 | memcpy(&volume, msg->volume, sizeof(volume)); 106 | 107 | g_ps.config.playback.volume(msg->nchannels, volume); 108 | return PS_STATUS_OK; 109 | } 110 | 111 | static PS_STATUS onMessage_playbackMute(PSChannel * channel) 112 | { 113 | SpiceMsgAudioMute * msg = (SpiceMsgAudioMute *)channel->buffer; 114 | 115 | g_ps.config.playback.mute(msg->mute); 116 | return PS_STATUS_OK; 117 | } 118 | 119 | PSHandlerFn channelPlayback_onMessage(PSChannel * channel) 120 | { 121 | channel->initDone = true; 122 | switch(channel->header.type) 123 | { 124 | case SPICE_MSG_PLAYBACK_START: 125 | return onMessage_playbackStart; 126 | 127 | //TODO: Lookup this message and see what it's for 128 | case SPICE_MSG_PLAYBACK_MODE: 129 | return PS_HANDLER_DISCARD; 130 | 131 | case SPICE_MSG_PLAYBACK_DATA: 132 | return onMessage_playbackData; 133 | 134 | case SPICE_MSG_PLAYBACK_STOP: 135 | return onMessage_playbackStop; 136 | 137 | case SPICE_MSG_PLAYBACK_VOLUME: 138 | if (!g_ps.config.playback.volume) 139 | return PS_HANDLER_DISCARD; 140 | return onMessage_playbackVolume; 141 | 142 | case SPICE_MSG_PLAYBACK_MUTE: 143 | if (!g_ps.config.playback.mute) 144 | return PS_HANDLER_DISCARD; 145 | return onMessage_playbackMute; 146 | } 147 | 148 | return PS_HANDLER_ERROR; 149 | } 150 | -------------------------------------------------------------------------------- /src/channel_playback.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | const SpiceLinkHeader * channelPlayback_getConnectPacket(void); 24 | 25 | PSHandlerFn channelPlayback_onMessage(PSChannel * channel); 26 | -------------------------------------------------------------------------------- /src/channel_record.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | 23 | #include "ps.h" 24 | #include "log.h" 25 | #include "channel.h" 26 | #include "channel_record.h" 27 | 28 | #include "messages.h" 29 | 30 | const SpiceLinkHeader * channelRecord_getConnectPacket(void) 31 | { 32 | typedef struct 33 | { 34 | SpiceLinkHeader header; 35 | SpiceLinkMess message; 36 | uint32_t supportCaps[COMMON_CAPS_BYTES / sizeof(uint32_t)]; 37 | uint32_t channelCaps[RECORD_CAPS_BYTES / sizeof(uint32_t)]; 38 | } 39 | __attribute__((packed)) ConnectPacket; 40 | 41 | static ConnectPacket p = 42 | { 43 | .header = { 44 | .magic = SPICE_MAGIC , 45 | .major_version = SPICE_VERSION_MAJOR, 46 | .minor_version = SPICE_VERSION_MINOR, 47 | .size = sizeof(ConnectPacket) - sizeof(SpiceLinkHeader) 48 | }, 49 | .message = { 50 | .channel_type = SPICE_CHANNEL_RECORD, 51 | .num_common_caps = COMMON_CAPS_BYTES / sizeof(uint32_t), 52 | .num_channel_caps = RECORD_CAPS_BYTES / sizeof(uint32_t), 53 | .caps_offset = sizeof(SpiceLinkMess) 54 | } 55 | }; 56 | 57 | p.message.connection_id = g_ps.sessionID; 58 | p.message.channel_id = g_ps.channelID; 59 | 60 | memset(p.supportCaps, 0, sizeof(p.supportCaps)); 61 | memset(p.channelCaps, 0, sizeof(p.channelCaps)); 62 | 63 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION); 64 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_AUTH_SPICE ); 65 | COMMON_SET_CAPABILITY(p.supportCaps, SPICE_COMMON_CAP_MINI_HEADER ); 66 | 67 | if (g_ps.config.record.volume || g_ps.config.record.mute) 68 | RECORD_SET_CAPABILITY(p.channelCaps, SPICE_RECORD_CAP_VOLUME); 69 | 70 | return &p.header; 71 | } 72 | 73 | static PS_STATUS onMessage_recordStart(PSChannel * channel) 74 | { 75 | SpiceMsgRecordStart * msg = (SpiceMsgRecordStart *)channel->buffer; 76 | 77 | PSAudioFormat fmt = PS_AUDIO_FMT_INVALID; 78 | if (msg->format == SPICE_AUDIO_FMT_S16) 79 | fmt = PS_AUDIO_FMT_S16; 80 | 81 | g_ps.config.record.start(msg->channels, msg->frequency, fmt); 82 | return PS_STATUS_OK; 83 | } 84 | 85 | static PS_STATUS onMessage_recordStop(PSChannel * channel) 86 | { 87 | (void)channel; 88 | 89 | g_ps.config.record.stop(); 90 | return PS_STATUS_OK; 91 | } 92 | 93 | static PS_STATUS onMessage_recordVolume(PSChannel * channel) 94 | { 95 | SpiceMsgAudioVolume * msg = (SpiceMsgAudioVolume *)channel->buffer; 96 | 97 | uint16_t volume[msg->nchannels]; 98 | memcpy(&volume, msg->volume, sizeof(volume)); 99 | 100 | g_ps.config.record.volume(msg->nchannels, volume); 101 | return PS_STATUS_OK; 102 | } 103 | 104 | static PS_STATUS onMessage_recordMute(PSChannel * channel) 105 | { 106 | SpiceMsgAudioMute * msg = (SpiceMsgAudioMute *)channel->buffer; 107 | 108 | g_ps.config.record.mute(msg->mute); 109 | return PS_STATUS_OK; 110 | } 111 | 112 | PSHandlerFn channelRecord_onMessage(PSChannel * channel) 113 | { 114 | channel->initDone = true; 115 | switch(channel->header.type) 116 | { 117 | case SPICE_MSG_RECORD_START: 118 | return onMessage_recordStart; 119 | 120 | case SPICE_MSG_RECORD_STOP: 121 | return onMessage_recordStop; 122 | 123 | case SPICE_MSG_RECORD_VOLUME: 124 | if (!g_ps.config.record.volume) 125 | return PS_HANDLER_DISCARD; 126 | return onMessage_recordVolume; 127 | 128 | case SPICE_MSG_RECORD_MUTE: 129 | if (!g_ps.config.record.mute) 130 | return PS_HANDLER_DISCARD; 131 | return onMessage_recordMute; 132 | } 133 | 134 | return PS_HANDLER_ERROR; 135 | } 136 | 137 | bool purespice_writeAudio(void * data, size_t size, uint32_t time) 138 | { 139 | PSChannel * channel = &g_ps.channels[PS_CHANNEL_RECORD]; 140 | if (!channel->connected) 141 | return false; 142 | 143 | SpiceMsgcRecordPacket * msg = 144 | SPICE_PACKET(SPICE_MSGC_RECORD_DATA, SpiceMsgcRecordPacket, size); 145 | 146 | msg->time = time; 147 | 148 | SPICE_LOCK(channel->lock); 149 | if (!SPICE_SEND_PACKET_NL(channel, msg)) 150 | { 151 | SPICE_UNLOCK(channel->lock); 152 | PS_LOG_ERROR("Failed to write SpiceMsgcRecordPacket"); 153 | return false; 154 | } 155 | const ssize_t wrote = send(channel->socket, data, size, 0); 156 | SPICE_UNLOCK(channel->lock); 157 | 158 | if ((size_t)wrote != size) 159 | { 160 | PS_LOG_ERROR("Failed to write the audio data"); 161 | return false; 162 | } 163 | 164 | return true; 165 | } 166 | -------------------------------------------------------------------------------- /src/channel_record.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "ps.h" 22 | 23 | const SpiceLinkHeader * channelRecord_getConnectPacket(void); 24 | 25 | PSHandlerFn channelRecord_onMessage(PSChannel * channel); 26 | -------------------------------------------------------------------------------- /src/draw.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef _H_I_DRAW_ 22 | #define _H_I_DRAW_ 23 | 24 | #include 25 | 26 | #pragma pack(push,1) 27 | 28 | typedef struct SpiceChunk 29 | { 30 | uint8_t *data; 31 | uint32_t len; 32 | } 33 | SpiceChunk; 34 | 35 | enum 36 | { 37 | SPICE_CHUNKS_FLAGS_UNSTABLE = (1<<0), 38 | SPICE_CHUNKS_FLAGS_FREE = (1<<1) 39 | }; 40 | 41 | typedef struct SpicePoint 42 | { 43 | int32_t x; 44 | int32_t y; 45 | } 46 | SpicePoint; 47 | 48 | typedef struct SpiceChunks 49 | { 50 | uint32_t data_size; 51 | uint32_t num_chunks; 52 | uint32_t flags; 53 | SpiceChunk chunk[]; 54 | } 55 | SpiceChunks; 56 | 57 | typedef struct SpicePalette { 58 | uint64_t unique; 59 | uint16_t num_ents; 60 | uint32_t ents[]; 61 | } 62 | SpicePalette; 63 | 64 | typedef struct SpiceSurface 65 | { 66 | uint32_t surface_id; 67 | } 68 | SpiceSurface; 69 | 70 | typedef struct SpiceBitmap 71 | { 72 | uint8_t format; 73 | uint8_t flags; 74 | uint32_t x; 75 | uint32_t y; 76 | uint32_t stride; 77 | SpicePalette * palette; 78 | uint64_t palette_id; 79 | uint8_t * data; 80 | } 81 | SpiceBitmap; 82 | 83 | typedef struct SpiceQUICData 84 | { 85 | uint32_t data_size; 86 | uint8_t data[]; 87 | } 88 | SpiceQUICData, 89 | SpiceLZRGBData, 90 | SpiceJPEGData, 91 | SpiceLZ4Data; 92 | 93 | typedef struct SpiceLZPLTData 94 | { 95 | uint8_t flags; 96 | uint32_t data_size; 97 | SpicePalette * palette; 98 | uint64_t palette_id; 99 | SpiceChunks * data; 100 | } 101 | SpiceLZPLTData; 102 | 103 | typedef struct SpiceZlibGlzRGBData 104 | { 105 | uint32_t glz_data_size; 106 | uint32_t data_size; 107 | SpiceChunks * data; 108 | } 109 | SpiceZlibGlzRGBData; 110 | 111 | typedef struct SpiceJPEGAlphaData 112 | { 113 | uint8_t flags; 114 | uint32_t jpeg_size; 115 | uint32_t data_size; 116 | SpiceChunks * data; 117 | } 118 | SpiceJPEGAlphaData; 119 | 120 | typedef struct SpiceImageDescriptor 121 | { 122 | uint64_t id; 123 | uint8_t type; 124 | uint8_t flags; 125 | uint32_t width; 126 | uint32_t height; 127 | } 128 | SpiceImageDescriptor; 129 | 130 | typedef struct SpiceImage 131 | { 132 | SpiceImageDescriptor descriptor; 133 | union 134 | { 135 | SpiceBitmap bitmap; 136 | // SpiceQUICData quic; 137 | // SpiceSurface surface; 138 | // SpiceLZRGBData lz_rgb; 139 | // SpiceLZPLTData lz_plt; 140 | // SpiceJPEGData jpeg; 141 | // SpiceLZ4Data lz4; 142 | // SpiceZlibGlzRGBData zlib_glz; 143 | // SpiceJPEGAlphaData jpeg_alpha; 144 | } 145 | u; 146 | } 147 | SpiceImage; 148 | 149 | typedef struct SpicePattern 150 | { 151 | SpiceImage * pat; 152 | SpicePoint pos; 153 | } 154 | SpicePattern; 155 | 156 | typedef struct SpiceBrush 157 | { 158 | uint32_t type; 159 | union 160 | { 161 | uint32_t color; 162 | SpicePattern pattern; 163 | } 164 | u; 165 | } 166 | SpiceBrush; 167 | 168 | typedef struct SpiceQMask 169 | { 170 | uint8_t flags; 171 | SpicePoint pos; 172 | SpiceImage * bitmap; 173 | } 174 | SpiceQMask; 175 | 176 | typedef struct SpiceFill 177 | { 178 | SpiceBrush brush; 179 | uint16_t rop_descriptor; 180 | SpiceQMask mask; 181 | } 182 | SpiceFill; 183 | 184 | typedef struct SpiceRect 185 | { 186 | int32_t top; 187 | int32_t left; 188 | int32_t bottom; 189 | int32_t right; 190 | } 191 | SpiceRect; 192 | 193 | typedef struct SpiceClipRects 194 | { 195 | uint32_t num_rects; 196 | SpiceRect rects[]; 197 | } 198 | SpiceClipRects; 199 | 200 | typedef struct SpiceClip 201 | { 202 | uint8_t type; 203 | SpiceClipRects * rects; 204 | } 205 | SpiceClip; 206 | 207 | typedef struct SpiceCopy 208 | { 209 | SpiceImage * src_bitmap; 210 | struct 211 | { 212 | SpiceRect src_area; 213 | uint16_t rop_descriptor; 214 | uint8_t scale_mode; 215 | } 216 | meta; 217 | SpiceQMask mask; 218 | } 219 | SpiceCopy, 220 | SpiceBlend; 221 | 222 | #pragma pack(pop) 223 | 224 | #endif 225 | -------------------------------------------------------------------------------- /src/locking.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef _H_SPICE_LOCKING_ 22 | #define _H_SPICE_LOCKING_ 23 | 24 | #include 25 | 26 | #define SPICE_LOCK_INIT(x) \ 27 | atomic_flag_clear(&(x)) 28 | 29 | #define SPICE_LOCK(x) \ 30 | while(atomic_flag_test_and_set_explicit(&(x), memory_order_acquire)) { ; } 31 | 32 | #define SPICE_UNLOCK(x) \ 33 | atomic_flag_clear_explicit(&(x), memory_order_release); 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/log.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef _H_I_LOG_ 22 | #define _H_I_LOG_ 23 | 24 | #include "log.h" 25 | #include "ps.h" 26 | 27 | #include 28 | #include 29 | 30 | static void log_stdout(const char * file, unsigned int line, 31 | const char * function, const char * format, ...) 32 | { 33 | va_list va; 34 | 35 | const char * f = strrchr(file, '/') + 1; 36 | fprintf(stdout, "%s:%d (%s): ", f, line, function); 37 | 38 | va_start(va, format); 39 | vfprintf(stdout, format, va); 40 | va_end(va); 41 | fputc('\n', stdout); 42 | } 43 | 44 | static void log_stderr(const char * file, unsigned int line, 45 | const char * function, const char * format, ...) 46 | { 47 | va_list va; 48 | 49 | const char * f = strrchr(file, '/') + 1; 50 | fprintf(stderr, "%s:%d (%s): ", f, line, function); 51 | 52 | va_start(va, format); 53 | vfprintf(stderr, format, va); 54 | va_end(va); 55 | fputc('\n', stderr); 56 | } 57 | 58 | void log_init(void) 59 | { 60 | if (!g_ps.init.log.info) 61 | g_ps.init.log.info = log_stdout; 62 | 63 | if (!g_ps.init.log.warn) 64 | g_ps.init.log.warn = log_stdout; 65 | 66 | if (!g_ps.init.log.error) 67 | g_ps.init.log.error = log_stderr; 68 | } 69 | 70 | #endif 71 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef _H_I_LOG_ 22 | #define _H_I_LOG_ 23 | 24 | #include "ps.h" 25 | 26 | #define _PS_LOG(func, fmt, ...) do { \ 27 | func(__FILE__, __LINE__, __FUNCTION__, fmt, ##__VA_ARGS__); \ 28 | } while(0); 29 | 30 | #define PS_LOG_INFO(fmt, ...) _PS_LOG(g_ps.init.log.info , fmt, ##__VA_ARGS__) 31 | #define PS_LOG_WARN(fmt, ...) _PS_LOG(g_ps.init.log.warn , fmt, ##__VA_ARGS__) 32 | #define PS_LOG_ERROR(fmt, ...) _PS_LOG(g_ps.init.log.error, fmt, ##__VA_ARGS__) 33 | 34 | #define PS_LOG_INFO_ONCE(fmt, ...) do { \ 35 | static char first = 1; \ 36 | if (first) \ 37 | { \ 38 | first = 0; \ 39 | PS_LOG_INFO(fmt, ##__VA_ARGS__) \ 40 | } \ 41 | } while(0) 42 | 43 | #define PS_LOG_WARN_ONCE(fmt, ...) do { \ 44 | static char first = 1; \ 45 | if (first) \ 46 | { \ 47 | first = 0; \ 48 | PS_LOG_WARN(fmt, ##__VA_ARGS__) \ 49 | } \ 50 | } while(0) 51 | 52 | #define PS_LOG_ERROR_ONCE(fmt, ...) do { \ 53 | static char first = 1; \ 54 | if (first) \ 55 | { \ 56 | first = 0; \ 57 | PS_LOG_ERROR(fmt, ##__VA_ARGS__) \ 58 | } \ 59 | } while(0) 60 | 61 | void log_init(void); 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /src/messages.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef _H_I_MESSAGES_ 22 | #define _H_I_MESSAGES_ 23 | 24 | #include 25 | #include 26 | #include "draw.h" 27 | 28 | #pragma pack(push,1) 29 | 30 | typedef struct SpicePoint16 31 | { 32 | int16_t x, y; 33 | } 34 | SpicePoint16; 35 | 36 | typedef struct SpiceMsgMainInit 37 | { 38 | uint32_t session_id; 39 | uint32_t display_channels_hint; 40 | uint32_t supported_mouse_modes; 41 | uint32_t current_mouse_mode; 42 | uint32_t agent_connected; 43 | uint32_t agent_tokens; 44 | uint32_t multi_media_time; 45 | uint32_t ram_hint; 46 | } 47 | SpiceMsgMainInit; 48 | 49 | typedef struct SpiceChannelID 50 | { 51 | uint8_t type; 52 | uint8_t channel_id; 53 | } 54 | SpiceChannelID; 55 | 56 | typedef struct SpiceMsgMainName 57 | { 58 | uint32_t name_len; 59 | uint8_t name[]; //name_len 60 | } 61 | SpiceMsgMainName; 62 | 63 | typedef struct SpiceMsgMainUUID 64 | { 65 | uint8_t uuid[16]; 66 | } 67 | SpiceMsgMainUUID; 68 | 69 | typedef struct SpiceMsgMainChannelsList 70 | { 71 | uint32_t num_of_channels; 72 | SpiceChannelID channels[]; 73 | } 74 | SpiceMainChannelsList; 75 | 76 | typedef struct SpiceMsgcMainMouseModeRequest 77 | { 78 | uint16_t mouse_mode; 79 | } 80 | SpiceMsgcMainMouseModeRequest; 81 | 82 | typedef struct SpiceMsgPing 83 | { 84 | uint32_t id; 85 | uint64_t timestamp; 86 | } 87 | SpiceMsgPing, 88 | SpiceMsgcPong; 89 | 90 | typedef struct SpiceMsgSetAck 91 | { 92 | uint32_t generation; 93 | uint32_t window; 94 | } 95 | SpiceMsgSetAck; 96 | 97 | typedef struct SpiceMsgcAckSync 98 | { 99 | uint32_t generation; 100 | } 101 | SpiceMsgcAckSync; 102 | 103 | typedef struct SpiceMsgNotify 104 | { 105 | uint64_t time_stamp; 106 | uint32_t severity; 107 | uint32_t visibility; 108 | uint32_t what; 109 | uint32_t message_len; 110 | char message[]; //message_len+1 111 | } 112 | SpiceMsgNotify; 113 | 114 | typedef struct SpiceMsgInputsInit 115 | { 116 | uint16_t modifiers; 117 | } 118 | SpiceMsgInputsInit, 119 | SpiceMsgInputsKeyModifiers, 120 | SpiceMsgcInputsKeyModifiers; 121 | 122 | typedef struct SpiceMsgcKeyDown 123 | { 124 | uint32_t code; 125 | } 126 | SpiceMsgcKeyDown, 127 | SpiceMsgcKeyUp; 128 | 129 | typedef struct SpiceMsgcMousePosition 130 | { 131 | uint32_t x; 132 | uint32_t y; 133 | uint16_t button_state; 134 | uint8_t display_id; 135 | } 136 | SpiceMsgcMousePosition; 137 | 138 | typedef struct SpiceMsgcMouseMotion 139 | { 140 | int32_t x; 141 | int32_t y; 142 | uint16_t button_state; 143 | } 144 | SpiceMsgcMouseMotion; 145 | 146 | typedef struct SpiceMsgcMousePress 147 | { 148 | uint8_t button; 149 | uint16_t button_state; 150 | } 151 | SpiceMsgcMousePress, 152 | SpiceMsgcMouseRelease; 153 | 154 | typedef struct SpiceMsgcDisconnecting 155 | { 156 | uint64_t time_stamp; 157 | uint32_t reason; 158 | } 159 | SpiceMsgcDisconnecting; 160 | 161 | typedef struct SpiceMsgPlaybackStart 162 | { 163 | uint32_t channels; 164 | SpiceAudioFmt format:16; 165 | uint32_t frequency; 166 | uint32_t time; 167 | } 168 | SpiceMsgPlaybackStart; 169 | 170 | typedef struct SpiceMsgRecordStart 171 | { 172 | uint32_t channels; 173 | uint16_t format; 174 | uint32_t frequency; 175 | } 176 | SpiceMsgRecordStart; 177 | 178 | typedef struct SpiceMsgPlaybackPacket 179 | { 180 | uint32_t time; 181 | uint8_t data[]; 182 | } 183 | SpiceMsgPlaybackPacket, 184 | SpiceMsgcRecordPacket; 185 | 186 | typedef struct SpiceMsgAudioVolume 187 | { 188 | uint8_t nchannels; 189 | uint16_t volume[]; 190 | } 191 | SpiceMsgAudioVolume; 192 | 193 | typedef struct SpiceMsgAudioMute 194 | { 195 | uint8_t mute; 196 | } 197 | SpiceMsgAudioMute; 198 | 199 | typedef struct SpiceMsgcPlaybackMode 200 | { 201 | uint32_t time; 202 | SpiceAudioDataMode mode:16; 203 | uint8_t data[]; 204 | } 205 | SpiceMsgPlaybackMode, 206 | SpiceMsgcRecordMode; 207 | 208 | typedef struct SpiceMsgcDisplayInit 209 | { 210 | uint8_t pixmap_cache_id; 211 | int64_t pixmap_cache_size; 212 | uint8_t glz_dictionary_id; 213 | uint32_t glz_dictionary_window_size; 214 | } 215 | SpiceMsgcDisplayInit; 216 | 217 | typedef struct SpiceMsgcPreferredCompression 218 | { 219 | uint8_t image_compression; 220 | } 221 | SpiceMsgcPreferredCompression; 222 | 223 | typedef struct SpiceMsgSurfaceCreate 224 | { 225 | uint32_t surface_id; 226 | uint32_t width; 227 | uint32_t height; 228 | uint32_t format; 229 | uint32_t flags; 230 | } 231 | SpiceMsgSurfaceCreate; 232 | 233 | typedef struct SpiceMsgSurfaceDestroy 234 | { 235 | uint32_t surface_id; 236 | } 237 | SpiceMsgSurfaceDestroy; 238 | 239 | typedef struct SpiceMsgDisplayBase 240 | { 241 | uint32_t surface_id; 242 | SpiceRect box; 243 | SpiceClip clip; 244 | } 245 | SpiceMsgDisplayBase; 246 | 247 | typedef struct SpiceMsgDisplayDrawFill 248 | { 249 | SpiceMsgDisplayBase base; 250 | SpiceFill data; 251 | } 252 | SpiceMsgDisplayDrawFill; 253 | 254 | typedef struct SpiceMsgDisplayDrawCopy 255 | { 256 | SpiceMsgDisplayBase base; 257 | SpiceCopy data; 258 | } 259 | SpiceMsgDisplayDrawCopy; 260 | 261 | typedef struct SpiceCursorHeader 262 | { 263 | uint64_t unique; 264 | uint8_t type; 265 | uint16_t width; 266 | uint16_t height; 267 | uint16_t hot_spot_x; 268 | uint16_t hot_spot_y; 269 | } 270 | SpiceCursorHeader; 271 | 272 | typedef struct SpiceCursor 273 | { 274 | uint16_t flags; 275 | SpiceCursorHeader header; 276 | uint8_t data[]; 277 | } 278 | SpiceCursor; 279 | 280 | typedef struct SpiceMsgCursorInit 281 | { 282 | SpicePoint16 position; 283 | uint16_t trail_length; 284 | uint16_t trail_frequency; 285 | uint8_t visible; 286 | SpiceCursor cursor; 287 | } 288 | SpiceMsgCursorInit; 289 | 290 | typedef struct SpiceMsgCursorSet 291 | { 292 | SpicePoint16 position; 293 | uint8_t visible; 294 | SpiceCursor cursor; 295 | } 296 | SpiceMsgCursorSet; 297 | 298 | typedef struct SpiceMsgCursorMove 299 | { 300 | SpicePoint16 position; 301 | } 302 | SpiceMsgCursorMove; 303 | 304 | typedef struct SpiceMsgCursorTrail 305 | { 306 | uint16_t length; 307 | uint16_t frequency; 308 | } 309 | SpiceMsgCursorTrail; 310 | 311 | typedef struct SpiceMsgCursorInvalOne 312 | { 313 | uint64_t cursor_id; 314 | } 315 | SpiceMsgCursorInvalOne; 316 | 317 | // spice is missing these defines, the offical reference library incorrectly 318 | // uses the VD defines 319 | 320 | #define HAS_CAPABILITY(caps, caps_size, index) \ 321 | ((index) < (caps_size * 32) && ((caps)[(index) / 32] & (1 << ((index) % 32)))) 322 | 323 | #define _SET_CAPABILITY(caps, index) \ 324 | { (caps)[(index) / 32] |= (1 << ((index) % 32)); } 325 | 326 | #define COMMON_CAPS_BYTES (((SPICE_COMMON_CAP_MINI_HEADER + 32) / 8) & ~3) 327 | #define COMMON_SET_CAPABILITY(caps, index) _SET_CAPABILITY(caps, index) 328 | 329 | #define MAIN_CAPS_BYTES (((SPICE_MAIN_CAP_SEAMLESS_MIGRATE + 32) / 8) & ~3) 330 | #define MAIN_SET_CAPABILITY(caps, index) _SET_CAPABILITY(caps, index) 331 | 332 | #define INPUT_CAPS_BYTES (((SPICE_INPUTS_CAP_KEY_SCANCODE + 32) / 8) & ~3) 333 | #define INPUT_SET_CAPABILITY(caps, index) _SET_CAPABILITY(caps, index) 334 | 335 | #define PLAYBACK_CAPS_BYTES (((SPICE_PLAYBACK_CAP_OPUS + 32) / 8) & ~3) 336 | #define PLAYBACK_SET_CAPABILITY(caps, index) _SET_CAPABILITY(caps, index) 337 | 338 | #define RECORD_CAPS_BYTES (((SPICE_RECORD_CAP_OPUS + 32) / 8) & ~3) 339 | #define RECORD_SET_CAPABILITY(caps, index) _SET_CAPABILITY(caps, index) 340 | 341 | #define DISPLAY_CAPS_BYTES (((SPICE_DISPLAY_CAP_CODEC_H265 + 32) / 8) & ~3) 342 | #define DISPLAY_SET_CAPABILITY(caps, index) _SET_CAPABILITY(caps, index) 343 | 344 | #define CURSOR_CAPS_BYTES (0) 345 | #define CUSROR_SET_CAPABILITY(caps, index) _SET_CAPABILITY(caps, index) 346 | 347 | #pragma pack(pop) 348 | 349 | #endif 350 | -------------------------------------------------------------------------------- /src/ps.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "purespice.h" 22 | 23 | #include "ps.h" 24 | #include "log.h" 25 | #include "agent.h" 26 | #include "channel.h" 27 | #include "channel_main.h" 28 | #include "channel_inputs.h" 29 | #include "channel_playback.h" 30 | #include "channel_record.h" 31 | #include "channel_display.h" 32 | #include "channel_cursor.h" 33 | 34 | #include "messages.h" 35 | #include "rsa.h" 36 | #include "queue.h" 37 | 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | #include 45 | #include 46 | 47 | #include 48 | 49 | // globals 50 | PS g_ps = 51 | { 52 | .channels = 53 | { 54 | // PS_CHANNEL_MAIN 55 | { 56 | .spiceType = SPICE_CHANNEL_MAIN, 57 | .name = "MAIN", 58 | .getConnectPacket = channelMain_getConnectPacket, 59 | .setCaps = channelMain_setCaps, 60 | .onMessage = channelMain_onMessage 61 | }, 62 | // PS_CHANNEL_INPUTS 63 | { 64 | .spiceType = SPICE_CHANNEL_INPUTS, 65 | .name = "INPUTS", 66 | .enable = &g_ps.config.inputs.enable, 67 | .autoConnect = &g_ps.config.inputs.autoConnect, 68 | .getConnectPacket = channelInputs_getConnectPacket, 69 | .onMessage = channelInputs_onMessage 70 | }, 71 | // PS_CHANNEL_PLAYBACK 72 | { 73 | .spiceType = SPICE_CHANNEL_PLAYBACK, 74 | .name = "PLAYBACK", 75 | .enable = &g_ps.config.playback.enable, 76 | .autoConnect = &g_ps.config.playback.autoConnect, 77 | .getConnectPacket = channelPlayback_getConnectPacket, 78 | .onMessage = channelPlayback_onMessage 79 | }, 80 | // PS_CHANNEL_RECORD 81 | { 82 | .spiceType = SPICE_CHANNEL_RECORD, 83 | .name = "RECORD", 84 | .enable = &g_ps.config.record.enable, 85 | .autoConnect = &g_ps.config.record.autoConnect, 86 | .getConnectPacket = channelRecord_getConnectPacket, 87 | .onMessage = channelRecord_onMessage, 88 | }, 89 | // PS_CHANNEL_DISPLAY 90 | { 91 | .spiceType = SPICE_CHANNEL_DISPLAY, 92 | .name = "DISPLAY", 93 | .enable = &g_ps.config.display.enable, 94 | .autoConnect = &g_ps.config.display.autoConnect, 95 | .getConnectPacket = channelDisplay_getConnectPacket, 96 | .onConnect = channelDisplay_onConnect, 97 | .onMessage = channelDisplay_onMessage 98 | }, 99 | // PS_CHANNEL_CURSOR 100 | { 101 | .spiceType = SPICE_CHANNEL_CURSOR, 102 | .name = "CURSOR", 103 | .enable = &g_ps.config.cursor.enable, 104 | .autoConnect = &g_ps.config.cursor.autoConnect, 105 | .getConnectPacket = channelCursor_getConnectPacket, 106 | .onMessage = channelCursor_onMessage 107 | } 108 | } 109 | }; 110 | 111 | void purespice_init(const PSInit * init) 112 | { 113 | if (init) 114 | memcpy(&g_ps.init, init, sizeof(*init)); 115 | log_init(); 116 | g_ps.initialized = true; 117 | } 118 | 119 | bool purespice_connect(const PSConfig * config) 120 | { 121 | if (!g_ps.initialized) 122 | { 123 | log_init(); 124 | g_ps.initialized = true; 125 | } 126 | 127 | memcpy(&g_ps.config, config, sizeof(*config)); 128 | 129 | g_ps.config.host = (const char *)strdup(config->host); 130 | if (!g_ps.config.host) 131 | { 132 | PS_LOG_ERROR("Failed to malloc"); 133 | goto err_host; 134 | } 135 | 136 | g_ps.config.password = (const char *)strdup(config->password); 137 | if (!g_ps.config.password) 138 | { 139 | PS_LOG_ERROR("Failed to malloc"); 140 | goto err_password; 141 | } 142 | 143 | if (g_ps.config.clipboard.enable) 144 | { 145 | if (!g_ps.config.clipboard.notice) 146 | { 147 | PS_LOG_ERROR("clipboard->notice is mandatory"); 148 | goto err_config; 149 | } 150 | 151 | if (!g_ps.config.clipboard.data) 152 | { 153 | PS_LOG_ERROR("clipboard->data is mandatory"); 154 | goto err_config; 155 | } 156 | 157 | if (!g_ps.config.clipboard.release) 158 | { 159 | PS_LOG_ERROR("clipboard->release is mandatory"); 160 | goto err_config; 161 | } 162 | 163 | if (!g_ps.config.clipboard.request) 164 | { 165 | PS_LOG_ERROR("clipboard->request is mandatory"); 166 | goto err_config; 167 | } 168 | } 169 | 170 | if (g_ps.config.playback.enable) 171 | { 172 | if (!g_ps.config.playback.start) 173 | { 174 | PS_LOG_ERROR("playback->start is mandatory"); 175 | goto err_config; 176 | } 177 | 178 | if (!g_ps.config.playback.stop) 179 | { 180 | PS_LOG_ERROR("playback->stop is mandatory"); 181 | goto err_config; 182 | } 183 | 184 | if (!g_ps.config.playback.data) 185 | { 186 | PS_LOG_ERROR("playback->data is mandatory"); 187 | goto err_config; 188 | } 189 | } 190 | 191 | if (g_ps.config.record.enable) 192 | { 193 | if (!g_ps.config.record.start) 194 | { 195 | PS_LOG_ERROR("record->start is mandatory"); 196 | goto err_config; 197 | } 198 | 199 | if (!g_ps.config.record.stop) 200 | { 201 | PS_LOG_ERROR("record->stop is mandatory"); 202 | goto err_config; 203 | } 204 | } 205 | 206 | if (g_ps.config.display.enable) 207 | { 208 | if (!g_ps.config.display.surfaceCreate) 209 | { 210 | PS_LOG_ERROR("display->surfaceCreate is mandatory"); 211 | goto err_config; 212 | } 213 | 214 | if (!g_ps.config.display.surfaceDestroy) 215 | { 216 | PS_LOG_ERROR("display->surfaceDestroy is mandatory"); 217 | goto err_config; 218 | } 219 | 220 | if (!g_ps.config.display.drawBitmap) 221 | { 222 | PS_LOG_ERROR("display->drawBitmap is mandatory"); 223 | goto err_config; 224 | } 225 | 226 | if (!g_ps.config.display.drawFill) 227 | { 228 | PS_LOG_ERROR("display->drawFill is mandatory"); 229 | goto err_config; 230 | } 231 | } 232 | 233 | memset(&g_ps.addr, 0, sizeof(g_ps.addr)); 234 | 235 | if (g_ps.config.port == 0) 236 | { 237 | PS_LOG_INFO("Connecting to unix socket %s", g_ps.config.host); 238 | 239 | g_ps.family = AF_UNIX; 240 | g_ps.addr.un.sun_family = g_ps.family; 241 | strncpy(g_ps.addr.un.sun_path, g_ps.config.host, 242 | sizeof(g_ps.addr.un.sun_path) - 1); 243 | } 244 | else 245 | { 246 | PS_LOG_INFO("Connecting to socket %s:%u", 247 | g_ps.config.host, g_ps.config.port); 248 | 249 | g_ps.family = AF_INET; 250 | inet_pton(g_ps.family, g_ps.config.host, &g_ps.addr.in.sin_addr); 251 | g_ps.addr.in.sin_family = g_ps.family; 252 | g_ps.addr.in.sin_port = htons(g_ps.config.port); 253 | } 254 | 255 | g_ps.epollfd = epoll_create1(0); 256 | if (g_ps.epollfd < 0) 257 | { 258 | PS_LOG_ERROR("epoll_create1 failed"); 259 | goto err_config; 260 | } 261 | 262 | g_ps.channelID = 0; 263 | if (channel_connect(&g_ps.channels[0]) != PS_STATUS_OK) 264 | { 265 | PS_LOG_ERROR("channel connect failed"); 266 | goto err_connect; 267 | } 268 | 269 | PS_LOG_INFO("Connected"); 270 | g_ps.connected = true; 271 | return true; 272 | 273 | err_connect: 274 | close(g_ps.epollfd); 275 | 276 | err_config: 277 | free((char *)g_ps.config.host); 278 | g_ps.config.host = NULL; 279 | 280 | err_password: 281 | free((char *)g_ps.config.password); 282 | g_ps.config.password = NULL; 283 | 284 | err_host: 285 | return false; 286 | } 287 | 288 | void purespice_disconnect(void) 289 | { 290 | if (!g_ps.initialized) 291 | { 292 | log_init(); 293 | g_ps.initialized = true; 294 | } 295 | 296 | const bool wasConnected = g_ps.connected; 297 | g_ps.connected = false; 298 | 299 | for(int i = PS_CHANNEL_MAX - 1; i >= 0; --i) 300 | channel_internal_disconnect(&g_ps.channels[i]); 301 | 302 | close(g_ps.epollfd); 303 | 304 | if (g_ps.motionBuffer) 305 | { 306 | free(g_ps.motionBuffer); 307 | g_ps.motionBuffer = NULL; 308 | } 309 | 310 | if (g_ps.config.host) 311 | { 312 | free((char *)g_ps.config.host); 313 | g_ps.config.host = NULL; 314 | } 315 | 316 | if (g_ps.config.password) 317 | { 318 | free((char *)g_ps.config.password); 319 | g_ps.config.password = NULL; 320 | } 321 | 322 | if (g_ps.guestName) 323 | { 324 | free(g_ps.guestName); 325 | g_ps.guestName = NULL; 326 | } 327 | 328 | agent_disconnect(); 329 | 330 | if (wasConnected) 331 | PS_LOG_INFO("Disconnected"); 332 | } 333 | 334 | PSStatus purespice_process(int timeout) 335 | { 336 | static struct epoll_event events[PS_CHANNEL_MAX]; 337 | 338 | // check for pending disconnects 339 | for(int i = 0; i < PS_CHANNEL_MAX; ++i) 340 | if (g_ps.channels[i].initDone && g_ps.channels[i].doDisconnect) 341 | channel_internal_disconnect(&g_ps.channels[i]); 342 | 343 | int nfds = epoll_wait(g_ps.epollfd, events, PS_CHANNEL_MAX, timeout); 344 | if (nfds == 0 || (nfds < 0 && errno == EINTR)) 345 | return PS_STATUS_RUN; 346 | 347 | if (nfds < 0) 348 | { 349 | if (!g_ps.connected) 350 | { 351 | PS_LOG_INFO("Shutdown"); 352 | return PS_STATUS_SHUTDOWN; 353 | } 354 | 355 | PS_LOG_ERROR("epoll_err returned %d", nfds); 356 | return PS_STATUS_ERR_POLL; 357 | } 358 | 359 | // process each channel one message at a time to avoid stalling a channel 360 | 361 | int done = 0; 362 | while(done < nfds) 363 | { 364 | for(int i = 0; i < nfds; ++i) 365 | { 366 | if (!events[i].data.ptr) 367 | continue; 368 | 369 | PSChannel * channel = (PSChannel *)events[i].data.ptr; 370 | 371 | int dataAvailable; 372 | ioctl(channel->socket, FIONREAD, &dataAvailable); 373 | 374 | // check if the socket has been disconnected 375 | if (!dataAvailable) 376 | goto done_disconnect; 377 | 378 | // if we don't have a header yet, read it 379 | if (channel->headerRead < sizeof(SpiceMiniDataHeader)) 380 | { 381 | int size = sizeof(SpiceMiniDataHeader) - channel->headerRead; 382 | uint8_t * dst = ((uint8_t *)&channel->header) + channel->headerRead; 383 | 384 | if (size > dataAvailable) 385 | size = dataAvailable; 386 | 387 | ssize_t len = read(channel->socket, dst, size); 388 | if (len == 0) 389 | goto done_disconnect; 390 | 391 | if (len < 0) 392 | { 393 | PS_LOG_ERROR("%s: Failed to read from the socket: %ld", 394 | channel->name, len); 395 | return PS_STATUS_ERR_READ; 396 | } 397 | 398 | // check if we have a complete header 399 | channel->headerRead += len; 400 | if (channel->headerRead < sizeof(SpiceMiniDataHeader)) 401 | continue; 402 | 403 | // ack that we got the message 404 | if (!channel_ack(channel)) 405 | { 406 | PS_LOG_ERROR("%s: Failed to send message ack", channel->name); 407 | return PS_STATUS_ERR_ACK; 408 | } 409 | 410 | dataAvailable -= len; 411 | channel->bufferRead = 0; 412 | if (channel->header.type < SPICE_MSG_BASE_LAST) 413 | channel->handlerFn = channel_onMessage(channel); 414 | else 415 | channel->handlerFn = channel->onMessage(channel); 416 | 417 | if (channel->handlerFn == PS_HANDLER_ERROR) 418 | { 419 | PS_LOG_ERROR("%s: invalid message: %d", 420 | channel->name, channel->header.type); 421 | return PS_STATUS_ERR_READ; 422 | } 423 | 424 | if (channel->handlerFn == PS_HANDLER_DISCARD) 425 | { 426 | channel->discarding = true; 427 | channel->discardSize = channel->header.size; 428 | } 429 | else 430 | { 431 | // ensure we have a large enough buffer to read the entire message 432 | if (channel->bufferSize < channel->header.size) 433 | { 434 | free(channel->buffer); 435 | channel->buffer = malloc(channel->header.size); 436 | if (!channel->buffer) 437 | { 438 | PS_LOG_ERROR("out of memory"); 439 | return PS_STATUS_ERR_READ; 440 | } 441 | channel->bufferSize = channel->header.size; 442 | } 443 | } 444 | } 445 | 446 | // check if we are discarding data 447 | if (channel->discarding) 448 | { 449 | while(channel->discardSize && dataAvailable) 450 | { 451 | char temp[8192]; 452 | unsigned int discard = 453 | channel->discardSize > (unsigned int)dataAvailable ? 454 | (unsigned int)dataAvailable : channel->discardSize; 455 | if (discard > sizeof(temp)) 456 | discard = sizeof(temp); 457 | 458 | ssize_t len = read(channel->socket, temp, discard); 459 | if (len == 0) 460 | goto done_disconnect; 461 | 462 | if (len < 0) 463 | { 464 | PS_LOG_ERROR("%s: Failed to discard from the socket: %ld", 465 | channel->name, len); 466 | return PS_STATUS_ERR_READ; 467 | } 468 | 469 | dataAvailable -= len; 470 | channel->discardSize -= len; 471 | } 472 | 473 | if (!channel->discardSize) 474 | { 475 | channel->discarding = false; 476 | channel->headerRead = 0; 477 | } 478 | } 479 | else 480 | { 481 | // read the payload into the buffer 482 | int size = channel->header.size - channel->bufferRead; 483 | if (size) 484 | { 485 | if (size > dataAvailable) 486 | size = dataAvailable; 487 | ssize_t len = read(channel->socket, 488 | channel->buffer + channel->bufferRead, size); 489 | 490 | if (len == 0) 491 | goto done_disconnect; 492 | 493 | if (len < 0) 494 | { 495 | PS_LOG_ERROR("%s: Failed to read the message payload: %ld", 496 | channel->name, len); 497 | return PS_STATUS_ERR_READ; 498 | } 499 | 500 | dataAvailable -= len; 501 | channel->bufferRead += len; 502 | } 503 | 504 | // if we have the full payload call the channel handler to process it 505 | if (channel->bufferRead == channel->header.size) 506 | { 507 | channel->headerRead = 0; 508 | 509 | // process the data 510 | switch(channel->handlerFn(channel)) 511 | { 512 | case PS_STATUS_OK: 513 | case PS_STATUS_HANDLED: 514 | break; 515 | 516 | case PS_STATUS_NODATA: 517 | goto done_disconnect; 518 | 519 | default: 520 | PS_LOG_ERROR("%s: Handler reported read error", channel->name); 521 | return PS_STATUS_ERR_READ; 522 | } 523 | } 524 | } 525 | 526 | // if there is no more data, we are finished processing this channel 527 | if (dataAvailable == 0) 528 | { 529 | ++done; 530 | events[i].data.ptr = NULL; 531 | } 532 | 533 | continue; 534 | 535 | done_disconnect: 536 | ++done; 537 | events[i].data.ptr = NULL; 538 | channel_internal_disconnect(channel); 539 | } 540 | } 541 | 542 | for(int i = 0; i < PS_CHANNEL_MAX; ++i) 543 | if (g_ps.channels[i].connected) 544 | return PS_STATUS_RUN; 545 | 546 | g_ps.sessionID = 0; 547 | 548 | for(int i = PS_CHANNEL_MAX - 1; i >= 0; --i) 549 | close(g_ps.channels[i].socket); 550 | 551 | PS_LOG_INFO("Shutdown"); 552 | return PS_STATUS_SHUTDOWN; 553 | } 554 | 555 | bool purespice_getServerInfo(PSServerInfo * info) 556 | { 557 | if (!g_ps.guestName) 558 | return false; 559 | 560 | memcpy(info->uuid, g_ps.guestUUID, sizeof(g_ps.guestUUID)); 561 | info->name = strdup(g_ps.guestName); 562 | 563 | return true; 564 | } 565 | 566 | void purespice_freeServerInfo(PSServerInfo * info) 567 | { 568 | if (!info) 569 | return; 570 | 571 | if (info->name) 572 | free(info->name); 573 | } 574 | 575 | static uint8_t channelTypeToSpiceType(PSChannelType channel) 576 | { 577 | switch(channel) 578 | { 579 | case PS_CHANNEL_MAIN: 580 | return SPICE_CHANNEL_MAIN; 581 | 582 | case PS_CHANNEL_INPUTS: 583 | return SPICE_CHANNEL_INPUTS; 584 | 585 | case PS_CHANNEL_PLAYBACK: 586 | return SPICE_CHANNEL_PLAYBACK; 587 | 588 | case PS_CHANNEL_RECORD: 589 | return SPICE_CHANNEL_RECORD; 590 | 591 | case PS_CHANNEL_DISPLAY: 592 | return SPICE_CHANNEL_DISPLAY; 593 | 594 | case PS_CHANNEL_CURSOR: 595 | return SPICE_CHANNEL_CURSOR; 596 | 597 | default: 598 | PS_LOG_ERROR("Invalid channel"); 599 | return 255; 600 | }; 601 | 602 | __builtin_unreachable(); 603 | } 604 | 605 | static PSChannel * getChannel(PSChannelType channel) 606 | { 607 | const uint8_t spiceType = channelTypeToSpiceType(channel); 608 | if (spiceType == 255) 609 | return NULL; 610 | 611 | for(int i = 0; i < PS_CHANNEL_MAX; ++i) 612 | if (g_ps.channels[i].spiceType == spiceType) 613 | return &g_ps.channels[i]; 614 | 615 | __builtin_unreachable(); 616 | } 617 | 618 | bool purespice_hasChannel(PSChannelType channel) 619 | { 620 | PSChannel * ch = getChannel(channel); 621 | if (!ch) 622 | return false; 623 | 624 | return ch->available; 625 | } 626 | 627 | bool purespice_channelConnected(PSChannelType channel) 628 | { 629 | PSChannel * ch = getChannel(channel); 630 | if (!ch) 631 | return false; 632 | return ch->connected; 633 | } 634 | 635 | PS_STATUS ps_connectChannel(PSChannel * ch) 636 | { 637 | PS_STATUS status; 638 | if ((status = channel_connect(ch)) != PS_STATUS_OK) 639 | { 640 | purespice_disconnect(); 641 | PS_LOG_ERROR("Failed to connect to the %s channel", ch->name); 642 | return status; 643 | } 644 | 645 | PS_LOG_INFO("%s channel connected", ch->name); 646 | if (ch->onConnect && (status = ch->onConnect(ch)) != PS_STATUS_OK) 647 | { 648 | purespice_disconnect(); 649 | PS_LOG_ERROR("Failed to connect to the %s channel", ch->name); 650 | return status; 651 | } 652 | 653 | return PS_STATUS_OK; 654 | } 655 | 656 | bool purespice_connectChannel(PSChannelType channel) 657 | { 658 | PSChannel * ch = getChannel(channel); 659 | if (!ch) 660 | return false; 661 | 662 | if (!ch->available) 663 | { 664 | PS_LOG_ERROR("%s: Channel is not availble", ch->name); 665 | return false; 666 | } 667 | 668 | if (ch->connected) 669 | return true; 670 | 671 | return ps_connectChannel(ch) == PS_STATUS_OK; 672 | } 673 | 674 | bool purespice_disconnectChannel(PSChannelType channel) 675 | { 676 | PSChannel * ch = getChannel(channel); 677 | if (!ch) 678 | return false; 679 | 680 | if (!ch->available) 681 | { 682 | PS_LOG_ERROR("%s: Channel is not availble", ch->name); 683 | return false; 684 | } 685 | 686 | if (!ch->connected) 687 | return true; 688 | 689 | channel_disconnect(ch); 690 | return true; 691 | } 692 | -------------------------------------------------------------------------------- /src/ps.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef _H_I_PURESPICE_ 22 | #define _H_I_PURESPICE_ 23 | 24 | #include "purespice.h" 25 | 26 | #include "locking.h" 27 | #include "messages.h" 28 | 29 | #include 30 | #include 31 | #include 32 | 33 | #include 34 | #include 35 | #include 36 | 37 | // we don't really need flow control because we are all local 38 | // instead do what the spice-gtk library does and provide the largest 39 | // possible number 40 | #define SPICE_AGENT_TOKENS_MAX ~0 41 | 42 | #define _SPICE_RAW_PACKET(htype, dataSize, extraData, _alloc) \ 43 | ({ \ 44 | uint8_t * packet = _alloc(sizeof(ssize_t) + \ 45 | sizeof(SpiceMiniDataHeader) + dataSize); \ 46 | ssize_t * sz = (ssize_t*)packet; \ 47 | SpiceMiniDataHeader * header = (SpiceMiniDataHeader *)(sz + 1); \ 48 | *sz = sizeof(SpiceMiniDataHeader) + dataSize; \ 49 | header->type = (htype); \ 50 | header->size = dataSize + extraData; \ 51 | (header + 1); \ 52 | }) 53 | 54 | #define SPICE_RAW_PACKET(htype, dataSize, extraData) \ 55 | _SPICE_RAW_PACKET(htype, dataSize, extraData, alloca) 56 | 57 | #define SPICE_RAW_PACKET_MALLOC(htype, dataSize, extraData) \ 58 | _SPICE_RAW_PACKET(htype, dataSize, extraData, malloc) 59 | 60 | #define SPICE_RAW_PACKET_FREE(packet) \ 61 | { \ 62 | SpiceMiniDataHeader * header = (SpiceMiniDataHeader *)(((uint8_t *)packet) - \ 63 | sizeof(SpiceMiniDataHeader)); \ 64 | ssize_t *sz = (ssize_t *)(((uint8_t *)header) - sizeof(ssize_t)); \ 65 | free(sz); \ 66 | } 67 | 68 | #define SPICE_SET_PACKET_SIZE(packet, sz) \ 69 | { \ 70 | SpiceMiniDataHeader * header = (SpiceMiniDataHeader *)(((uint8_t *)packet) - \ 71 | sizeof(SpiceMiniDataHeader)); \ 72 | header->size = sz; \ 73 | } 74 | 75 | #define SPICE_PACKET(htype, payloadType, extraData) \ 76 | ((payloadType *)SPICE_RAW_PACKET(htype, sizeof(payloadType), extraData)) 77 | 78 | #define SPICE_PACKET_MALLOC(htype, payloadType, extraData) \ 79 | ((payloadType *)SPICE_RAW_PACKET_MALLOC(htype, sizeof(payloadType), extraData)) 80 | 81 | #define SPICE_SEND_PACKET(channel, packet) \ 82 | ({ \ 83 | SpiceMiniDataHeader * header = (SpiceMiniDataHeader *)(((uint8_t *)packet) - \ 84 | sizeof(SpiceMiniDataHeader)); \ 85 | ssize_t *sz = (ssize_t *)(((uint8_t *)header) - sizeof(ssize_t)); \ 86 | SPICE_LOCK((channel)->lock); \ 87 | const ssize_t wrote = send((channel)->socket, header, *sz, 0); \ 88 | SPICE_UNLOCK((channel)->lock); \ 89 | wrote == *sz; \ 90 | }) 91 | 92 | #define SPICE_SEND_PACKET_NL(channel, packet) \ 93 | ({ \ 94 | SpiceMiniDataHeader * header = (SpiceMiniDataHeader *)(((uint8_t *)packet) - \ 95 | sizeof(SpiceMiniDataHeader)); \ 96 | ssize_t *sz = (ssize_t *)(((uint8_t *)header) - sizeof(ssize_t)); \ 97 | const ssize_t wrote = send((channel)->socket, header, *sz, 0); \ 98 | wrote == *sz; \ 99 | }) 100 | 101 | // currently (2020) these defines are not yet availble for most distros, so we 102 | // just define them ourselfs for now 103 | #define _SPICE_MOUSE_BUTTON_SIDE 6 104 | #define _SPICE_MOUSE_BUTTON_EXTRA 7 105 | #define _SPICE_MOUSE_BUTTON_MASK_SIDE (1 << 5) 106 | #define _SPICE_MOUSE_BUTTON_MASK_EXTRA (1 << 6) 107 | 108 | typedef enum 109 | { 110 | PS_STATUS_OK, 111 | PS_STATUS_HANDLED, 112 | PS_STATUS_NODATA, 113 | PS_STATUS_ERROR 114 | } 115 | PS_STATUS; 116 | 117 | typedef struct PS PS; 118 | typedef struct PSChannel PSChannel; 119 | 120 | typedef PS_STATUS (*PSHandlerFn)(PSChannel * channel); 121 | #define PS_HANDLER_DISCARD (PSHandlerFn)( 0) 122 | #define PS_HANDLER_ERROR (PSHandlerFn)(-1) 123 | 124 | // internal structures 125 | struct PSChannel 126 | { 127 | uint8_t spiceType; 128 | const char * name; 129 | bool available; 130 | bool * enable; 131 | bool * autoConnect; 132 | 133 | SpiceMiniDataHeader header; 134 | unsigned int headerRead; 135 | PSHandlerFn handlerFn; 136 | uint8_t * buffer; 137 | unsigned int bufferSize; 138 | unsigned int bufferRead; 139 | bool discarding; 140 | unsigned int discardSize; 141 | 142 | const SpiceLinkHeader * (*getConnectPacket)(void); 143 | void (*setCaps)( 144 | const uint32_t * common , int numCommon, 145 | const uint32_t * channel, int numChannel); 146 | PS_STATUS (*onConnect)(PSChannel * channel); 147 | PSHandlerFn (*onMessage)(PSChannel * channel); 148 | 149 | bool connected; 150 | bool ready; 151 | bool doDisconnect; 152 | bool initDone; 153 | int socket; 154 | uint32_t ackFrequency; 155 | uint32_t ackCount; 156 | atomic_flag lock; 157 | }; 158 | 159 | struct PSCursorImage 160 | { 161 | struct PSCursorImage * next; 162 | bool cached; 163 | SpiceCursorHeader header; 164 | uint8_t buffer[]; 165 | }; 166 | 167 | struct PS 168 | { 169 | bool initialized; 170 | PSInit init; 171 | PSConfig config; 172 | 173 | short family; 174 | union 175 | { 176 | struct sockaddr addr; 177 | struct sockaddr_in in; 178 | struct sockaddr_in6 in6; 179 | struct sockaddr_un un; 180 | } 181 | addr; 182 | 183 | uint32_t sessionID; 184 | uint32_t channelID; 185 | char * guestName; 186 | uint8_t guestUUID[16]; 187 | 188 | bool connected; 189 | int epollfd; 190 | PSChannel channels[PS_CHANNEL_MAX]; 191 | bool channelsReady; 192 | 193 | struct 194 | { 195 | uint32_t modifiers; 196 | } 197 | kb; 198 | 199 | struct 200 | { 201 | atomic_flag lock; 202 | uint32_t buttonState; 203 | 204 | atomic_int sentCount; 205 | int rpos, wpos; 206 | } 207 | mouse; 208 | 209 | struct 210 | { 211 | uint16_t x, y; 212 | uint16_t trailLen, trailFreq; 213 | bool visible; 214 | struct PSCursorImage * cache; 215 | struct PSCursorImage ** cacheLast; 216 | struct PSCursorImage * current; 217 | } 218 | cursor; 219 | 220 | uint8_t * motionBuffer; 221 | size_t motionBufferSize; 222 | }; 223 | 224 | extern PS g_ps; 225 | 226 | PS_STATUS ps_connectChannel(PSChannel * ch); 227 | 228 | PS_STATUS purespice_onCommonRead(PSChannel * channel, 229 | SpiceMiniDataHeader * header, int * dataAvailable); 230 | 231 | #endif 232 | -------------------------------------------------------------------------------- /src/queue.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "queue.h" 22 | #include "locking.h" 23 | 24 | #include 25 | #include 26 | 27 | struct QueueItem 28 | { 29 | void * data; 30 | struct QueueItem * next; 31 | }; 32 | 33 | struct Queue 34 | { 35 | struct QueueItem * head; 36 | struct QueueItem * tail; 37 | struct QueueItem * pos; 38 | unsigned int count; 39 | atomic_flag lock; 40 | }; 41 | 42 | struct Queue * queue_new(void) 43 | { 44 | struct Queue * list = malloc(sizeof(struct Queue)); 45 | list->head = NULL; 46 | list->tail = NULL; 47 | list->pos = NULL; 48 | list->count = 0; 49 | SPICE_LOCK_INIT(list->lock); 50 | return list; 51 | } 52 | 53 | void queue_free(struct Queue * list) 54 | { 55 | // never free a list with items in it! 56 | assert(!list->head); 57 | free(list); 58 | } 59 | 60 | void queue_push(struct Queue * list, void * data) 61 | { 62 | struct QueueItem * item = malloc(sizeof(struct QueueItem)); 63 | item->data = data; 64 | item->next = NULL; 65 | 66 | SPICE_LOCK(list->lock); 67 | ++list->count; 68 | 69 | if (!list->head) 70 | { 71 | list->head = item; 72 | list->tail = item; 73 | SPICE_UNLOCK(list->lock); 74 | return; 75 | } 76 | 77 | list->tail->next = item; 78 | list->tail = item; 79 | SPICE_UNLOCK(list->lock); 80 | } 81 | 82 | bool queue_shift(struct Queue * list, void ** data) 83 | { 84 | SPICE_LOCK(list->lock); 85 | if (!list->head) 86 | { 87 | SPICE_UNLOCK(list->lock); 88 | return false; 89 | } 90 | 91 | --list->count; 92 | struct QueueItem * item = list->head; 93 | list->head = item->next; 94 | list->pos = NULL; 95 | if (list->tail == item) 96 | list->tail = NULL; 97 | 98 | SPICE_UNLOCK(list->lock); 99 | 100 | if (data) 101 | *data = item->data; 102 | 103 | free(item); 104 | return true; 105 | } 106 | 107 | bool queue_peek(struct Queue * list, void ** data) 108 | { 109 | if (!list->head) 110 | return false; 111 | 112 | struct QueueItem * item = list->head; 113 | if (data) 114 | *data = item->data; 115 | 116 | return true; 117 | } 118 | -------------------------------------------------------------------------------- /src/queue.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #ifndef _H_SPICE_QUEUE_ 22 | #define _H_SPICE_QUEUE_ 23 | 24 | #include 25 | 26 | struct Queue; 27 | 28 | struct Queue * queue_new(void); 29 | void queue_free(struct Queue * list); 30 | void queue_push(struct Queue * list, void * data); 31 | bool queue_shift(struct Queue * list, void ** data); 32 | bool queue_peek(struct Queue * list, void ** data); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/rsa.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include "rsa.h" 22 | #include "log.h" 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | #if defined(USE_OPENSSL) && defined(USE_NETTLE) 29 | #error "USE_OPENSSL and USE_NETTLE are both defined" 30 | #elif !defined(USE_OPENSSL) && !defined(USE_NETTLE) 31 | #error "One of USE_OPENSSL or USE_NETTLE must be defined" 32 | #endif 33 | 34 | #if defined(USE_OPENSSL) 35 | #include 36 | #include 37 | #include 38 | #endif 39 | 40 | #if defined(USE_NETTLE) 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | 48 | #define SHA1_HASH_LEN 20 49 | #endif 50 | 51 | #if defined(USE_NETTLE) 52 | /* the below OAEP implementation is derived from the FreeTDS project */ 53 | static void memxor(uint8_t * a, const uint8_t * b, const unsigned int len) 54 | { 55 | for(unsigned int i = 0; i < len; ++i) 56 | a[i] = a[i] ^ b[i]; 57 | } 58 | 59 | static void sha1(uint8_t * hash, const uint8_t *data, unsigned int len) 60 | { 61 | struct sha1_ctx ctx; 62 | 63 | sha1_init(&ctx); 64 | sha1_update(&ctx, len, data); 65 | sha1_digest(&ctx, SHA1_HASH_LEN, hash); 66 | } 67 | 68 | static void oaep_mask(uint8_t * dest, size_t dest_len, 69 | const uint8_t * mask, size_t mask_len) 70 | { 71 | uint8_t hash[SHA1_HASH_LEN]; 72 | uint8_t * seed = alloca(mask_len + 4); 73 | memcpy(seed, mask, mask_len); 74 | 75 | for(unsigned int n = 0;; ++n) 76 | { 77 | (seed+mask_len)[0] = n >> 24; 78 | (seed+mask_len)[1] = n >> 16; 79 | (seed+mask_len)[2] = n >> 8; 80 | (seed+mask_len)[3] = n >> 0; 81 | 82 | sha1(hash, seed, mask_len + 4); 83 | if (dest_len <= SHA1_HASH_LEN) 84 | { 85 | memxor(dest, hash, dest_len); 86 | break; 87 | } 88 | 89 | memxor(dest, hash, SHA1_HASH_LEN); 90 | dest += SHA1_HASH_LEN; 91 | dest_len -= SHA1_HASH_LEN; 92 | } 93 | } 94 | 95 | static bool oaep_pad(mpz_t m, unsigned int key_size, 96 | const uint8_t * message, unsigned int len) 97 | { 98 | if (len + SHA1_HASH_LEN * 2 + 2 > key_size) 99 | return false; 100 | 101 | struct 102 | { 103 | uint8_t zero; 104 | uint8_t ros[SHA1_HASH_LEN]; 105 | uint8_t db []; 106 | } 107 | * em; 108 | 109 | const int emSize = sizeof(*em) + key_size - SHA1_HASH_LEN - 1; 110 | em = alloca(emSize); 111 | memset(em, 0, emSize); 112 | 113 | sha1(em->db, (uint8_t *)"", 0); 114 | ((uint8_t *)em)[key_size - len - 1] = 0x1; 115 | memcpy((uint8_t *)em + (key_size - len), message, len); 116 | 117 | /* we are not too worried about randomness since we are just making a local 118 | * connection, should anyone use this code outside of LookingGlass please be 119 | * sure to use something better such as `gnutls_rnd` */ 120 | for(int i = 0; i < SHA1_HASH_LEN; ++i) 121 | em->ros[i] = rand() % 255; 122 | 123 | const int db_len = key_size - SHA1_HASH_LEN - 1; 124 | oaep_mask(em->db , db_len , em->ros, SHA1_HASH_LEN); 125 | oaep_mask(em->ros, SHA1_HASH_LEN, em->db , db_len ); 126 | 127 | nettle_mpz_set_str_256_u(m, key_size, (uint8_t*)em); 128 | return true; 129 | } 130 | #endif 131 | 132 | bool rsa_encryptPassword(uint8_t * pub_key, const char * password, 133 | PSPassword * result) 134 | { 135 | result->size = 0; 136 | result->data = NULL; 137 | 138 | #if defined(USE_OPENSSL) 139 | PS_LOG_INFO_ONCE("Using OpenSSL"); 140 | 141 | BIO *bioKey = BIO_new(BIO_s_mem()); 142 | if (!bioKey) 143 | { 144 | PS_LOG_ERROR("BIO_new failed"); 145 | return false; 146 | } 147 | 148 | BIO_write(bioKey, pub_key, SPICE_TICKET_PUBKEY_BYTES); 149 | EVP_PKEY *rsaKey = d2i_PUBKEY_bio(bioKey, NULL); 150 | RSA *rsa = EVP_PKEY_get1_RSA(rsaKey); 151 | 152 | result->size = RSA_size(rsa); 153 | result->data = (char *)malloc(result->size); 154 | 155 | if (RSA_public_encrypt( 156 | strlen(password) + 1, 157 | (const uint8_t*)password, 158 | (uint8_t*)result->data, 159 | rsa, 160 | RSA_PKCS1_OAEP_PADDING 161 | ) <= 0) 162 | { 163 | free(result->data); 164 | result->size = 0; 165 | result->data = NULL; 166 | 167 | EVP_PKEY_free(rsaKey); 168 | BIO_free(bioKey); 169 | PS_LOG_ERROR("RSA_public_encrypt failed"); 170 | return false; 171 | } 172 | 173 | EVP_PKEY_free(rsaKey); 174 | BIO_free(bioKey); 175 | return true; 176 | #endif 177 | 178 | #if defined(USE_NETTLE) 179 | PS_LOG_INFO_ONCE("Using Nettle"); 180 | 181 | struct asn1_der_iterator der; 182 | struct asn1_der_iterator j; 183 | struct rsa_public_key pub; 184 | 185 | if (asn1_der_iterator_first(&der, SPICE_TICKET_PUBKEY_BYTES, pub_key) 186 | == ASN1_ITERATOR_CONSTRUCTED 187 | && der.type == ASN1_SEQUENCE 188 | && asn1_der_decode_constructed_last(&der) == ASN1_ITERATOR_CONSTRUCTED 189 | && der.type == ASN1_SEQUENCE 190 | && asn1_der_decode_constructed(&der, &j) == ASN1_ITERATOR_PRIMITIVE 191 | && j.type == ASN1_IDENTIFIER 192 | && asn1_der_iterator_next(&der) == ASN1_ITERATOR_PRIMITIVE 193 | && der.type == ASN1_BITSTRING 194 | && asn1_der_decode_bitstring_last(&der)) 195 | { 196 | if (j.length != 9) 197 | { 198 | PS_LOG_ERROR("asn1 length invalid"); 199 | return false; 200 | } 201 | 202 | if (asn1_der_iterator_next(&j) == ASN1_ITERATOR_PRIMITIVE 203 | && j.type == ASN1_NULL 204 | && j.length == 0 205 | && asn1_der_iterator_next(&j) == ASN1_ITERATOR_END) 206 | { 207 | rsa_public_key_init(&pub); 208 | if (!rsa_public_key_from_der_iterator(&pub, 0, &der)) 209 | { 210 | rsa_public_key_clear(&pub); 211 | PS_LOG_ERROR("rsa_public_key_from_der_iterator failed"); 212 | return false; 213 | } 214 | } 215 | } 216 | else 217 | { 218 | PS_LOG_ERROR("failed to parse asn1 header"); 219 | return false; 220 | } 221 | 222 | mpz_t p; 223 | mpz_init(p); 224 | oaep_pad(p, pub.size, (const uint8_t *)password, strlen(password)+1); 225 | mpz_powm(p, p, pub.e, pub.n); 226 | 227 | result->size = pub.size; 228 | result->data = malloc(pub.size); 229 | nettle_mpz_get_str_256(pub.size, (uint8_t *)result->data, p); 230 | 231 | rsa_public_key_clear(&pub); 232 | mpz_clear(p); 233 | return true; 234 | #endif 235 | } 236 | 237 | void rsa_freePassword(PSPassword * pass) 238 | { 239 | free(pass->data); 240 | pass->size = 0; 241 | pass->data = NULL; 242 | } 243 | -------------------------------------------------------------------------------- /src/rsa.h: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include 22 | #include 23 | 24 | typedef struct PSPassword 25 | { 26 | char * data; 27 | unsigned int size; 28 | } 29 | PSPassword; 30 | 31 | bool rsa_encryptPassword(uint8_t * pub_key, const char * password, 32 | PSPassword * result); 33 | void rsa_freePassword(PSPassword * pass); 34 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | set(TARGET_NAME "spice-test") 3 | project(${TARGET_NAME}) 4 | 5 | include_directories(include) 6 | set(SOURCES main.c) 7 | 8 | add_compile_options( 9 | "-Wall" 10 | "-Werror" 11 | "-Wfatal-errors" 12 | "-ffast-math" 13 | "-fdata-sections" 14 | "-ffunction-sections" 15 | "$<$:-O0;-g3;-ggdb>" 16 | ) 17 | 18 | get_filename_component(PROJECT_TOP "${PROJECT_SOURCE_DIR}/.." ABSOLUTE) 19 | add_subdirectory("${PROJECT_TOP}" "${CMAKE_BINARY_DIR}/spice") 20 | add_subdirectory("ADL" "${CMAKE_BINARY_DIR}/ADL") 21 | 22 | add_executable(${TARGET_NAME} ${SOURCES}) 23 | target_link_libraries(${TARGET_NAME} 24 | adl 25 | purespice 26 | m 27 | ) 28 | -------------------------------------------------------------------------------- /test/main.c: -------------------------------------------------------------------------------- 1 | /** 2 | * PureSpice - A pure C implementation of the SPICE client protocol 3 | * Copyright © 2017-2025 Geoffrey McRae 4 | * https://github.com/gnif/PureSpice 5 | * 6 | * This program is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License as published by the Free 8 | * Software Foundation; either version 2 of the License, or (at your option) 9 | * any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | * more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 59 18 | * Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | 29 | bool connectionReady = false; 30 | bool record = false; 31 | int recordChannels; 32 | int recordSampleRate; 33 | double recordVolume[2]; 34 | int16_t * recordAudio; 35 | int recordAudioSize; 36 | 37 | static void clipboard_notice(const PSDataType type) 38 | { 39 | printf("clipboard_notice(type: %d)\n", type); 40 | } 41 | 42 | static void clipboard_data(const PSDataType type, uint8_t * buffer, 43 | uint32_t size) 44 | { 45 | printf("clipboard_data(type: %d, buffer: %p, size: %u)\n", 46 | type, buffer, size); 47 | } 48 | 49 | static void clipboard_release(void) 50 | { 51 | printf("clipboard_release\n"); 52 | } 53 | 54 | static void clipboard_request(const PSDataType type) 55 | { 56 | printf("clipboard_request\n"); 57 | } 58 | 59 | static void playback_start(int channels, int sampleRate, PSAudioFormat format, 60 | uint32_t time) 61 | { 62 | printf("playback_start(ch: %d, sampleRate: %d, format: %d, time: %u)\n", 63 | channels, sampleRate, format, time); 64 | } 65 | 66 | static void playback_volume(int channels, const uint16_t volume[]) 67 | { 68 | printf("playback_volume(ch: %d ", channels); 69 | for(int i = 0; i < channels; ++i) 70 | printf(", %d: %u", i, volume[i]); 71 | puts(")"); 72 | } 73 | 74 | static void playback_mute(bool mute) 75 | { 76 | printf("playback_mute(%d)\n", mute); 77 | } 78 | 79 | static void playback_stop(void) 80 | { 81 | printf("playback_stop\n"); 82 | } 83 | 84 | static void playback_data(uint8_t * data, size_t size) 85 | { 86 | printf("playback_data(%p, %lu)\n", data, size); 87 | } 88 | 89 | static void genSine() 90 | { 91 | if (recordAudio) 92 | free(recordAudio); 93 | 94 | #define FREQ 200 95 | 96 | recordAudioSize = recordSampleRate * sizeof(*recordAudio) * recordChannels; 97 | recordAudio = malloc(recordAudioSize); 98 | 99 | const double delta = 2.0 * M_PI * FREQ/(double)recordSampleRate; 100 | double acc = 0.0; 101 | for(int i = 0; i < recordSampleRate; ++i, acc += delta) 102 | { 103 | for(int c = 0; c < recordChannels; ++c) 104 | { 105 | double v = recordVolume[c] * sin(acc) * 32768.0; 106 | if (v < -32768) v = 32768; 107 | else if (v > 32767) v = 32767; 108 | recordAudio[i * recordChannels + c] = (int16_t)v; 109 | } 110 | } 111 | } 112 | 113 | static void record_start(int channels, int sampleRate, PSAudioFormat format) 114 | { 115 | printf("record_start(ch: %d, sampleRate: %d, format: %d)\n", 116 | channels, sampleRate, format); 117 | record = true; 118 | recordChannels = channels; 119 | recordSampleRate = sampleRate; 120 | genSine(); 121 | } 122 | 123 | static void record_volume(int channels, const uint16_t volume[]) 124 | { 125 | printf("record_volume(ch: %d ", channels); 126 | for(int i = 0; i < channels; ++i) 127 | { 128 | printf(", %d: %u", i, volume[i]); 129 | recordVolume[i] = 9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787; 130 | } 131 | puts(")"); 132 | genSine(); 133 | } 134 | 135 | static void record_mute(bool mute) 136 | { 137 | printf("record_mute(%d)\n", mute); 138 | record = !mute; 139 | } 140 | 141 | static void record_stop(void) 142 | { 143 | printf("record_stop\n"); 144 | record = false; 145 | } 146 | 147 | static void connection_ready(void) 148 | { 149 | printf("ready\n"); 150 | connectionReady = true; 151 | } 152 | 153 | FILE * fp = NULL; 154 | int dispWidth, dispHeight; 155 | 156 | typedef struct __attribute((packed))__ 157 | { 158 | uint16_t type; // Magic identifier: 0x4d42 159 | uint32_t size; // File size in bytes 160 | uint16_t reserved1; // Not used 161 | uint16_t reserved2; // Not used 162 | uint32_t offset; // Offset to image data in bytes from beginning of file (54 bytes) 163 | uint32_t dib_header_size; // DIB Header size in bytes (40 bytes) 164 | int32_t width_px; // Width of the image 165 | int32_t height_px; // Height of image 166 | uint16_t num_planes; // Number of color planes 167 | uint16_t bits_per_pixel; // Bits per pixel 168 | uint32_t compression; // Compression type 169 | uint32_t image_size_bytes; // Image size in bytes 170 | int32_t x_resolution_ppm; // Pixels per meter 171 | int32_t y_resolution_ppm; // Pixels per meter 172 | uint32_t num_colors; // Number of colors 173 | uint32_t important_colors; // Important colors 174 | } 175 | BMPHeader; 176 | 177 | 178 | static void display_surfaceCreate(unsigned int surfaceId, PSSurfaceFormat format, 179 | unsigned int width, unsigned int height) 180 | { 181 | printf("display_surfaceCreate(%u, %d, %u, %u)\n", 182 | surfaceId, format, width, height); 183 | 184 | dispWidth = width; 185 | dispHeight = height; 186 | fp = fopen("/tmp/dump.bmp", "wb"); 187 | fseek(fp, 0, SEEK_SET); 188 | 189 | BMPHeader h = 190 | { 191 | .type = 0x4d42, 192 | .size = sizeof(BMPHeader) + height * width * 4, 193 | .offset = sizeof(BMPHeader), 194 | .dib_header_size = 40, 195 | .width_px = width, 196 | .height_px = height, 197 | .num_planes = 1, 198 | .bits_per_pixel = 32, 199 | .image_size_bytes = height * width * 4, 200 | .x_resolution_ppm = 0, 201 | .y_resolution_ppm = 0, 202 | }; 203 | 204 | fwrite(&h, sizeof(h), 1, fp); 205 | } 206 | 207 | static void display_surfaceDestroy(unsigned int surfaceId) 208 | { 209 | printf("display_surfaceDestroy(%u)\n", surfaceId); 210 | 211 | if (fp) 212 | { 213 | fclose(fp); 214 | fp = NULL; 215 | } 216 | } 217 | 218 | static void display_drawFill(unsigned int surfaceId, 219 | int x, int y, 220 | int width, int height, 221 | uint32_t color) 222 | { 223 | printf("display_drawFill(%d, %d, %d, %d, 0x%08x)\n", 224 | x, y, 225 | width, height, 226 | color); 227 | } 228 | 229 | static void display_drawBitmap(unsigned int surfaceId, 230 | PSBitmapFormat format, 231 | bool topDown, 232 | int x, int y, 233 | int width, int height, 234 | int stride, 235 | void * data) 236 | { 237 | if (topDown) 238 | { 239 | uint8_t * src = (uint8_t *)data; 240 | for(int i = 0; i < height; ++i) 241 | { 242 | int dst = (dispWidth * 4 * (dispHeight-(y+i))) + x * 4; 243 | fseek(fp, sizeof(BMPHeader) + dst, SEEK_SET); 244 | fwrite(src, stride, 1, fp); 245 | src += stride; 246 | } 247 | } 248 | else 249 | { 250 | uint8_t * src = (uint8_t *)data + height * stride; 251 | for(int i = 0; i < height; ++i) 252 | { 253 | int dst = (dispWidth * 4 * (dispHeight-(y+i))) + x * 4; 254 | fseek(fp, sizeof(BMPHeader) + dst, SEEK_SET); 255 | src -= stride; 256 | fwrite(src, stride, 1, fp); 257 | } 258 | } 259 | fflush(fp); 260 | } 261 | 262 | int main(int argc, char * argv[]) 263 | { 264 | char * host; 265 | int port = 5900; 266 | #if 0 267 | if (argc < 2) 268 | { 269 | printf("Usage: %s host [port]\n", argv[0]); 270 | return -1; 271 | } 272 | 273 | host = argv[1]; 274 | if (argc > 2) 275 | port = atoi(argv[2]); 276 | #endif 277 | host = "/opt/PVM/vms/Windows/windows.sock"; 278 | port = 0; 279 | 280 | int retval = 0; 281 | 282 | if (adlInitialize() != ADL_OK) 283 | { 284 | retval = -1; 285 | goto err_exit; 286 | } 287 | 288 | { 289 | int count; 290 | adlGetPlatformList(&count, NULL); 291 | 292 | const char * platforms[count]; 293 | adlGetPlatformList(&count, platforms); 294 | 295 | if (adlUsePlatform(platforms[0]) != ADL_OK) 296 | { 297 | retval = -1; 298 | goto err_exit; 299 | } 300 | } 301 | 302 | const PSConfig config = 303 | { 304 | .host = host, 305 | .port = port, 306 | .password = "", 307 | .ready = connection_ready, 308 | .inputs = 309 | { 310 | .enable = true, 311 | .autoConnect = true 312 | }, 313 | .clipboard = 314 | { 315 | .enable = true, 316 | .notice = clipboard_notice, 317 | .data = clipboard_data, 318 | .release = clipboard_release, 319 | .request = clipboard_request 320 | }, 321 | .playback = 322 | { 323 | .enable = true, 324 | .autoConnect = true, 325 | .start = playback_start, 326 | .volume = playback_volume, 327 | .mute = playback_mute, 328 | .stop = playback_stop, 329 | .data = playback_data 330 | }, 331 | .record = { 332 | .enable = true, 333 | .autoConnect = true, 334 | .start = record_start, 335 | .mute = record_mute, 336 | .volume = record_volume, 337 | .stop = record_stop 338 | }, 339 | .display = { 340 | .enable = true, 341 | .autoConnect = false, 342 | .surfaceCreate = display_surfaceCreate, 343 | .surfaceDestroy = display_surfaceDestroy, 344 | .drawFill = display_drawFill, 345 | .drawBitmap = display_drawBitmap 346 | } 347 | }; 348 | 349 | if (!purespice_connect(&config)) 350 | { 351 | printf("spice connect failed\n"); 352 | retval = -1; 353 | goto err_exit; 354 | } 355 | 356 | /* wait for purespice to be ready */ 357 | while(!connectionReady) 358 | if (purespice_process(1) != PS_STATUS_RUN) 359 | { 360 | retval = -1; 361 | goto err_exit; 362 | } 363 | 364 | /* Create the parent window */ 365 | ADLWindowDef winDef = 366 | { 367 | .title = "PureSpice Test", 368 | .className = "purespice-test", 369 | .type = ADL_WINDOW_TYPE_DIALOG, 370 | .flags = 0, 371 | .borderless = false, 372 | .x = 0 , .y = 0 , 373 | .w = 200, .h = 200 374 | }; 375 | ADLWindow * parent; 376 | if (adlWindowCreate(winDef, &parent) != ADL_OK) 377 | { 378 | retval = -1; 379 | goto err_shutdown; 380 | } 381 | 382 | /* show the windows */ 383 | adlWindowShow(parent); 384 | adlFlush(); 385 | 386 | /* Process events */ 387 | ADLEvent event; 388 | ADL_STATUS status; 389 | while((status = adlProcessEvent(1, &event)) == ADL_OK) 390 | { 391 | switch(event.type) 392 | { 393 | case ADL_EVENT_NONE: 394 | if (purespice_process(1) != PS_STATUS_RUN) 395 | goto err_shutdown; 396 | 397 | if (record) 398 | purespice_writeAudio((uint8_t*)recordAudio, recordAudioSize, 0); 399 | continue; 400 | 401 | case ADL_EVENT_CLOSE: 402 | case ADL_EVENT_QUIT: 403 | goto exit; 404 | 405 | case ADL_EVENT_KEY_DOWN: 406 | if (purespice_channelConnected(PS_CHANNEL_DISPLAY)) 407 | { 408 | printf("Disconnect display\n"); 409 | purespice_disconnectChannel(PS_CHANNEL_DISPLAY); 410 | } 411 | else 412 | { 413 | printf("Connect display\n"); 414 | purespice_connectChannel(PS_CHANNEL_DISPLAY); 415 | } 416 | break; 417 | 418 | case ADL_EVENT_KEY_UP: 419 | break; 420 | 421 | case ADL_EVENT_MOUSE_DOWN: 422 | break; 423 | 424 | case ADL_EVENT_MOUSE_UP: 425 | break; 426 | 427 | case ADL_EVENT_MOUSE_MOVE: 428 | purespice_mouseMotion(event.u.mouse.relX, event.u.mouse.relY); 429 | break; 430 | 431 | default: 432 | break; 433 | } 434 | } 435 | 436 | exit: 437 | purespice_disconnect(); 438 | 439 | err_shutdown: 440 | adlShutdown(); 441 | err_exit: 442 | return retval; 443 | } 444 | --------------------------------------------------------------------------------