├── .gitignore ├── .readthedocs.yml ├── Jenkinsfile ├── LICENSE.txt ├── Makefile ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── bugs.rst │ ├── conf.py │ ├── index.rst │ ├── installation.rst │ ├── license.rst │ ├── modules.rst │ ├── rcon.battleye.rst │ ├── rcon.rst │ ├── rcon.source.rst │ └── usage.rst ├── rcon ├── __init__.py ├── battleye │ ├── __init__.py │ ├── client.py │ └── proto.py ├── client.py ├── config.py ├── console.py ├── errorhandler.py ├── exceptions.py ├── gui.py ├── rconclt.py ├── rconshell.py ├── readline.py └── source │ ├── __init__.py │ ├── async_rcon.py │ ├── client.py │ └── proto.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_battleye_proto.py ├── test_config.py ├── test_local_minecraft_server.py └── test_source_proto.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Run pytest') { 5 | steps { 6 | sh 'pip install --user --upgrade pytest' 7 | sh 'pip install --user --upgrade -r requirements.txt' 8 | sh 'python -m pytest' 9 | } 10 | } 11 | 12 | stage('Run SonarQube') { 13 | steps { 14 | withSonarQubeEnv(installationName: 'rcon', credentialsId: '4cdfb484-a052-41be-8739-3e1c232b5f38') { 15 | sh '/opt/sonar-scanner/bin/sonar-scanner' 16 | } 17 | 18 | } 19 | } 20 | 21 | stage('Send Email') { 22 | steps { 23 | mail(subject: '[rcon] build successful', body: 'https://jenkins.richard-neumann.de/blue/organizations/jenkins/rcon/activity', from: 'jenkins@richard-neumann.de', to: 'mail@richard-neumann.de') 24 | } 25 | } 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FILE_LIST = ./.installed_files.txt 2 | 3 | .PHONY: build clean install publish pull push uninstall 4 | 5 | build: 6 | @ ./setup.py sdist bdist_wheel 7 | 8 | clean: 9 | @ rm -Rf ./build ./dist 10 | 11 | default: | pull clean install 12 | 13 | install: 14 | @ ./setup.py install --record $(FILE_LIST) 15 | 16 | publish: 17 | @ twine upload dist/* 18 | 19 | pull: 20 | @ git pull 21 | 22 | push: 23 | @ git push 24 | 25 | pypi: | clean build publish 26 | 27 | uninstall: 28 | @ while read FILE; do echo "Removing: $$FILE"; rm "$$FILE"; done < $(FILE_LIST) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation Status](https://readthedocs.org/projects/rcon/badge/?version=latest)](https://rcon.readthedocs.io/en/latest/) 2 | 3 | # rcon 4 | An RCON client implementation. 5 | * [Source RCON protocol](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol) 6 | * [BattlEye RCon protocol](https://www.battleye.com/downloads/BERConProtocol.txt) 7 | 8 | ## Requirements 9 | `rcon` requires Python 3.10 or higher. 10 | 11 | ## Documentation 12 | Documentation is available on [readthedocs](https://rcon.readthedocs.io/en/latest/). 13 | 14 | ## Installation 15 | Install rcon from the [AUR](https://aur.archlinux.org/packages/python-rcon/) or via: 16 | 17 | pip install rcon 18 | 19 | ## Quick start 20 | The `RCON` protocols are used to remotely control game servers, i.e. execute 21 | commands on a game server and receive the respective results. 22 | 23 | ### Source RCON 24 | ```python 25 | from rcon.source import Client 26 | 27 | with Client('127.0.0.1', 5000, passwd='mysecretpassword') as client: 28 | response = client.run('some_command', 'with', 'some', 'arguments') 29 | 30 | print(response) 31 | ``` 32 | 33 | #### Async support 34 | If you prefer to use Source RCON in an asynchronous environment, you can use 35 | `rcon()`. 36 | 37 | ```python 38 | from rcon.source import rcon 39 | 40 | response = await rcon( 41 | 'some_command', 'with', 'some', 'arguments', 42 | host='127.0.0.1', port=5000, passwd='mysecretpassword' 43 | ) 44 | print(response) 45 | ``` 46 | 47 | ### BattlEye RCon 48 | ```python 49 | from rcon.battleye import Client 50 | 51 | with Client('127.0.0.1', 5000, passwd='mysecretpassword') as client: 52 | response = client.run('some_command', 'with', 'some', 'arguments') 53 | 54 | print(response) 55 | ``` 56 | 57 | #### Handling server messages 58 | Since the BattlEye RCon server will also send server messages to the client 59 | alongside command responses, you can register an event handler to process 60 | those messages: 61 | 62 | ```python 63 | from rcon.battleye import Client 64 | from rcon.battleye.proto import ServerMessage 65 | 66 | def my_message_handler(server_message: ServerMessage) -> None: 67 | """Print server messages.""" 68 | 69 | print('Server message:', server_message) 70 | 71 | with Client( 72 | '127.0.0.1', 73 | 5000, 74 | passwd='mysecretpassword', 75 | message_handler=my_message_handler 76 | ) as client: 77 | response = client.run('some_command', 'with', 'some', 'arguments') 78 | 79 | print('Response:', response) 80 | ``` 81 | 82 | Have a look at `rcon.battleye.proto.ServerMessage` for details on the 83 | respective objects. 84 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/bugs.rst: -------------------------------------------------------------------------------- 1 | Reporting bugs 2 | ============== 3 | 4 | If you found a bug or want to suggest a new feature to be added top `rcon`, please open an appropriate issue on `GitHub `_. 5 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "rcon" 10 | copyright = "2022, Richard Neumann" 11 | author = "Richard Neumann" 12 | release = "2.3.8" 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = ["sphinx.ext.autodoc"] 18 | 19 | templates_path = ["_templates"] 20 | exclude_patterns = [] 21 | 22 | 23 | # -- Options for HTML output ------------------------------------------------- 24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 25 | 26 | html_theme = "alabaster" 27 | html_static_path = ["_static"] 28 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | rcon - An RCON client library 2 | ============================= 3 | 4 | `rcon` is a Python 3 library, which provides a client to interact with RCON servers. 5 | It therefor implements the `RCON `_ protocol. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Contents: 10 | 11 | installation 12 | usage 13 | modules 14 | bugs 15 | license 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | If you use `Arch Linux` or one of its derivatives, you can install `rcon` from the `AUR `_. 5 | Otherwise you can install `rcon` via pip: 6 | 7 | .. code-block:: bash 8 | 9 | pip install rcon 10 | 11 | 12 | Requirements 13 | ============ 14 | 15 | `rcon` requires Python 3.8 or higher. 16 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | GNU GENERAL PUBLIC LICENSE 5 | -------------------------- 6 | 7 | Version 3, 29 June 2007 8 | 9 | Copyright (C) 2007 Free Software Foundation, Inc. 10 | 11 | 12 | Everyone is permitted to copy and distribute verbatim copies of this 13 | license document, but changing it is not allowed. 14 | 15 | Preamble 16 | -------- 17 | 18 | The GNU General Public License is a free, copyleft license for 19 | software and other kinds of works. 20 | 21 | The licenses for most software and other practical works are designed 22 | to take away your freedom to share and change the works. By contrast, 23 | the GNU General Public License is intended to guarantee your freedom 24 | to share and change all versions of a program--to make sure it remains 25 | free software for all its users. We, the Free Software Foundation, use 26 | the GNU General Public License for most of our software; it applies 27 | also to any other work released this way by its authors. You can apply 28 | it to your programs, too. 29 | 30 | When we speak of free software, we are referring to freedom, not 31 | price. Our General Public Licenses are designed to make sure that you 32 | have the freedom to distribute copies of free software (and charge for 33 | them if you wish), that you receive source code or can get it if you 34 | want it, that you can change the software or use pieces of it in new 35 | free programs, and that you know you can do these things. 36 | 37 | To protect your rights, we need to prevent others from denying you 38 | these rights or asking you to surrender the rights. Therefore, you 39 | have certain responsibilities if you distribute copies of the 40 | software, or if you modify it: responsibilities to respect the freedom 41 | of others. 42 | 43 | For example, if you distribute copies of such a program, whether 44 | gratis or for a fee, you must pass on to the recipients the same 45 | freedoms that you received. You must make sure that they, too, receive 46 | or can get the source code. And you must show them these terms so they 47 | know their rights. 48 | 49 | Developers that use the GNU GPL protect your rights with two steps: 50 | (1) assert copyright on the software, and (2) offer you this License 51 | giving you legal permission to copy, distribute and/or modify it. 52 | 53 | For the developers' and authors' protection, the GPL clearly explains 54 | that there is no warranty for this free software. For both users' and 55 | authors' sake, the GPL requires that modified versions be marked as 56 | changed, so that their problems will not be attributed erroneously to 57 | authors of previous versions. 58 | 59 | Some devices are designed to deny users access to install or run 60 | modified versions of the software inside them, although the 61 | manufacturer can do so. This is fundamentally incompatible with the 62 | aim of protecting users' freedom to change the software. The 63 | systematic pattern of such abuse occurs in the area of products for 64 | individuals to use, which is precisely where it is most unacceptable. 65 | Therefore, we have designed this version of the GPL to prohibit the 66 | practice for those products. If such problems arise substantially in 67 | other domains, we stand ready to extend this provision to those 68 | domains in future versions of the GPL, as needed to protect the 69 | freedom of users. 70 | 71 | Finally, every program is threatened constantly by software patents. 72 | States should not allow patents to restrict development and use of 73 | software on general-purpose computers, but in those that do, we wish 74 | to avoid the special danger that patents applied to a free program 75 | could make it effectively proprietary. To prevent this, the GPL 76 | assures that patents cannot be used to render the program non-free. 77 | 78 | The precise terms and conditions for copying, distribution and 79 | modification follow. 80 | 81 | TERMS AND CONDITIONS 82 | -------------------- 83 | 84 | 0. Definitions. 85 | ^^^^^^^^^^^^^^^ 86 | 87 | "This License" refers to version 3 of the GNU General Public License. 88 | 89 | "Copyright" also means copyright-like laws that apply to other kinds 90 | of works, such as semiconductor masks. 91 | 92 | "The Program" refers to any copyrightable work licensed under this 93 | License. Each licensee is addressed as "you". "Licensees" and 94 | "recipients" may be individuals or organizations. 95 | 96 | To "modify" a work means to copy from or adapt all or part of the work 97 | in a fashion requiring copyright permission, other than the making of 98 | an exact copy. The resulting work is called a "modified version" of 99 | the earlier work or a work "based on" the earlier work. 100 | 101 | A "covered work" means either the unmodified Program or a work based 102 | on the Program. 103 | 104 | To "propagate" a work means to do anything with it that, without 105 | permission, would make you directly or secondarily liable for 106 | infringement under applicable copyright law, except executing it on a 107 | computer or modifying a private copy. Propagation includes copying, 108 | distribution (with or without modification), making available to the 109 | public, and in some countries other activities as well. 110 | 111 | To "convey" a work means any kind of propagation that enables other 112 | parties to make or receive copies. Mere interaction with a user 113 | through a computer network, with no transfer of a copy, is not 114 | conveying. 115 | 116 | An interactive user interface displays "Appropriate Legal Notices" to 117 | the extent that it includes a convenient and prominently visible 118 | feature that (1) displays an appropriate copyright notice, and (2) 119 | tells the user that there is no warranty for the work (except to the 120 | extent that warranties are provided), that licensees may convey the 121 | work under this License, and how to view a copy of this License. If 122 | the interface presents a list of user commands or options, such as a 123 | menu, a prominent item in the list meets this criterion. 124 | 125 | 1. Source Code. 126 | ^^^^^^^^^^^^^^^ 127 | 128 | The "source code" for a work means the preferred form of the work for 129 | making modifications to it. "Object code" means any non-source form of 130 | a work. 131 | 132 | A "Standard Interface" means an interface that either is an official 133 | standard defined by a recognized standards body, or, in the case of 134 | interfaces specified for a particular programming language, one that 135 | is widely used among developers working in that language. 136 | 137 | The "System Libraries" of an executable work include anything, other 138 | than the work as a whole, that (a) is included in the normal form of 139 | packaging a Major Component, but which is not part of that Major 140 | Component, and (b) serves only to enable use of the work with that 141 | Major Component, or to implement a Standard Interface for which an 142 | implementation is available to the public in source code form. A 143 | "Major Component", in this context, means a major essential component 144 | (kernel, window system, and so on) of the specific operating system 145 | (if any) on which the executable work runs, or a compiler used to 146 | produce the work, or an object code interpreter used to run it. 147 | 148 | The "Corresponding Source" for a work in object code form means all 149 | the source code needed to generate, install, and (for an executable 150 | work) run the object code and to modify the work, including scripts to 151 | control those activities. However, it does not include the work's 152 | System Libraries, or general-purpose tools or generally available free 153 | programs which are used unmodified in performing those activities but 154 | which are not part of the work. For example, Corresponding Source 155 | includes interface definition files associated with source files for 156 | the work, and the source code for shared libraries and dynamically 157 | linked subprograms that the work is specifically designed to require, 158 | such as by intimate data communication or control flow between those 159 | subprograms and other parts of the work. 160 | 161 | The Corresponding Source need not include anything that users can 162 | regenerate automatically from other parts of the Corresponding Source. 163 | 164 | The Corresponding Source for a work in source code form is that same 165 | work. 166 | 167 | 2. Basic Permissions. 168 | ^^^^^^^^^^^^^^^^^^^^^ 169 | 170 | All rights granted under this License are granted for the term of 171 | copyright on the Program, and are irrevocable provided the stated 172 | conditions are met. This License explicitly affirms your unlimited 173 | permission to run the unmodified Program. The output from running a 174 | covered work is covered by this License only if the output, given its 175 | content, constitutes a covered work. This License acknowledges your 176 | rights of fair use or other equivalent, as provided by copyright law. 177 | 178 | You may make, run and propagate covered works that you do not convey, 179 | without conditions so long as your license otherwise remains in force. 180 | You may convey covered works to others for the sole purpose of having 181 | them make modifications exclusively for you, or provide you with 182 | facilities for running those works, provided that you comply with the 183 | terms of this License in conveying all material for which you do not 184 | control copyright. Those thus making or running the covered works for 185 | you must do so exclusively on your behalf, under your direction and 186 | control, on terms that prohibit them from making any copies of your 187 | copyrighted material outside their relationship with you. 188 | 189 | Conveying under any other circumstances is permitted solely under the 190 | conditions stated below. Sublicensing is not allowed; section 10 makes 191 | it unnecessary. 192 | 193 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 194 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 195 | 196 | No covered work shall be deemed part of an effective technological 197 | measure under any applicable law fulfilling obligations under article 198 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 199 | similar laws prohibiting or restricting circumvention of such 200 | measures. 201 | 202 | When you convey a covered work, you waive any legal power to forbid 203 | circumvention of technological measures to the extent such 204 | circumvention is effected by exercising rights under this License with 205 | respect to the covered work, and you disclaim any intention to limit 206 | operation or modification of the work as a means of enforcing, against 207 | the work's users, your or third parties' legal rights to forbid 208 | circumvention of technological measures. 209 | 210 | 4. Conveying Verbatim Copies. 211 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 212 | 213 | You may convey verbatim copies of the Program's source code as you 214 | receive it, in any medium, provided that you conspicuously and 215 | appropriately publish on each copy an appropriate copyright notice; 216 | keep intact all notices stating that this License and any 217 | non-permissive terms added in accord with section 7 apply to the code; 218 | keep intact all notices of the absence of any warranty; and give all 219 | recipients a copy of this License along with the Program. 220 | 221 | You may charge any price or no price for each copy that you convey, 222 | and you may offer support or warranty protection for a fee. 223 | 224 | 5. Conveying Modified Source Versions. 225 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 226 | 227 | You may convey a work based on the Program, or the modifications to 228 | produce it from the Program, in the form of source code under the 229 | terms of section 4, provided that you also meet all of these 230 | conditions: 231 | 232 | - a) The work must carry prominent notices stating that you modified 233 | it, and giving a relevant date. 234 | - b) The work must carry prominent notices stating that it is 235 | released under this License and any conditions added under 236 | section 7. This requirement modifies the requirement in section 4 237 | to "keep intact all notices". 238 | - c) You must license the entire work, as a whole, under this 239 | License to anyone who comes into possession of a copy. This 240 | License will therefore apply, along with any applicable section 7 241 | additional terms, to the whole of the work, and all its parts, 242 | regardless of how they are packaged. This License gives no 243 | permission to license the work in any other way, but it does not 244 | invalidate such permission if you have separately received it. 245 | - d) If the work has interactive user interfaces, each must display 246 | Appropriate Legal Notices; however, if the Program has interactive 247 | interfaces that do not display Appropriate Legal Notices, your 248 | work need not make them do so. 249 | 250 | A compilation of a covered work with other separate and independent 251 | works, which are not by their nature extensions of the covered work, 252 | and which are not combined with it such as to form a larger program, 253 | in or on a volume of a storage or distribution medium, is called an 254 | "aggregate" if the compilation and its resulting copyright are not 255 | used to limit the access or legal rights of the compilation's users 256 | beyond what the individual works permit. Inclusion of a covered work 257 | in an aggregate does not cause this License to apply to the other 258 | parts of the aggregate. 259 | 260 | 6. Conveying Non-Source Forms. 261 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 262 | 263 | You may convey a covered work in object code form under the terms of 264 | sections 4 and 5, provided that you also convey the machine-readable 265 | Corresponding Source under the terms of this License, in one of these 266 | ways: 267 | 268 | - a) Convey the object code in, or embodied in, a physical product 269 | (including a physical distribution medium), accompanied by the 270 | Corresponding Source fixed on a durable physical medium 271 | customarily used for software interchange. 272 | - b) Convey the object code in, or embodied in, a physical product 273 | (including a physical distribution medium), accompanied by a 274 | written offer, valid for at least three years and valid for as 275 | long as you offer spare parts or customer support for that product 276 | model, to give anyone who possesses the object code either (1) a 277 | copy of the Corresponding Source for all the software in the 278 | product that is covered by this License, on a durable physical 279 | medium customarily used for software interchange, for a price no 280 | more than your reasonable cost of physically performing this 281 | conveying of source, or (2) access to copy the Corresponding 282 | Source from a network server at no charge. 283 | - c) Convey individual copies of the object code with a copy of the 284 | written offer to provide the Corresponding Source. This 285 | alternative is allowed only occasionally and noncommercially, and 286 | only if you received the object code with such an offer, in accord 287 | with subsection 6b. 288 | - d) Convey the object code by offering access from a designated 289 | place (gratis or for a charge), and offer equivalent access to the 290 | Corresponding Source in the same way through the same place at no 291 | further charge. You need not require recipients to copy the 292 | Corresponding Source along with the object code. If the place to 293 | copy the object code is a network server, the Corresponding Source 294 | may be on a different server (operated by you or a third party) 295 | that supports equivalent copying facilities, provided you maintain 296 | clear directions next to the object code saying where to find the 297 | Corresponding Source. Regardless of what server hosts the 298 | Corresponding Source, you remain obligated to ensure that it is 299 | available for as long as needed to satisfy these requirements. 300 | - e) Convey the object code using peer-to-peer transmission, 301 | provided you inform other peers where the object code and 302 | Corresponding Source of the work are being offered to the general 303 | public at no charge under subsection 6d. 304 | 305 | A separable portion of the object code, whose source code is excluded 306 | from the Corresponding Source as a System Library, need not be 307 | included in conveying the object code work. 308 | 309 | A "User Product" is either (1) a "consumer product", which means any 310 | tangible personal property which is normally used for personal, 311 | family, or household purposes, or (2) anything designed or sold for 312 | incorporation into a dwelling. In determining whether a product is a 313 | consumer product, doubtful cases shall be resolved in favor of 314 | coverage. For a particular product received by a particular user, 315 | "normally used" refers to a typical or common use of that class of 316 | product, regardless of the status of the particular user or of the way 317 | in which the particular user actually uses, or expects or is expected 318 | to use, the product. A product is a consumer product regardless of 319 | whether the product has substantial commercial, industrial or 320 | non-consumer uses, unless such uses represent the only significant 321 | mode of use of the product. 322 | 323 | "Installation Information" for a User Product means any methods, 324 | procedures, authorization keys, or other information required to 325 | install and execute modified versions of a covered work in that User 326 | Product from a modified version of its Corresponding Source. The 327 | information must suffice to ensure that the continued functioning of 328 | the modified object code is in no case prevented or interfered with 329 | solely because modification has been made. 330 | 331 | If you convey an object code work under this section in, or with, or 332 | specifically for use in, a User Product, and the conveying occurs as 333 | part of a transaction in which the right of possession and use of the 334 | User Product is transferred to the recipient in perpetuity or for a 335 | fixed term (regardless of how the transaction is characterized), the 336 | Corresponding Source conveyed under this section must be accompanied 337 | by the Installation Information. But this requirement does not apply 338 | if neither you nor any third party retains the ability to install 339 | modified object code on the User Product (for example, the work has 340 | been installed in ROM). 341 | 342 | The requirement to provide Installation Information does not include a 343 | requirement to continue to provide support service, warranty, or 344 | updates for a work that has been modified or installed by the 345 | recipient, or for the User Product in which it has been modified or 346 | installed. Access to a network may be denied when the modification 347 | itself materially and adversely affects the operation of the network 348 | or violates the rules and protocols for communication across the 349 | network. 350 | 351 | Corresponding Source conveyed, and Installation Information provided, 352 | in accord with this section must be in a format that is publicly 353 | documented (and with an implementation available to the public in 354 | source code form), and must require no special password or key for 355 | unpacking, reading or copying. 356 | 357 | 7. Additional Terms. 358 | ^^^^^^^^^^^^^^^^^^^^ 359 | 360 | "Additional permissions" are terms that supplement the terms of this 361 | License by making exceptions from one or more of its conditions. 362 | Additional permissions that are applicable to the entire Program shall 363 | be treated as though they were included in this License, to the extent 364 | that they are valid under applicable law. If additional permissions 365 | apply only to part of the Program, that part may be used separately 366 | under those permissions, but the entire Program remains governed by 367 | this License without regard to the additional permissions. 368 | 369 | When you convey a copy of a covered work, you may at your option 370 | remove any additional permissions from that copy, or from any part of 371 | it. (Additional permissions may be written to require their own 372 | removal in certain cases when you modify the work.) You may place 373 | additional permissions on material, added by you to a covered work, 374 | for which you have or can give appropriate copyright permission. 375 | 376 | Notwithstanding any other provision of this License, for material you 377 | add to a covered work, you may (if authorized by the copyright holders 378 | of that material) supplement the terms of this License with terms: 379 | 380 | - a) Disclaiming warranty or limiting liability differently from the 381 | terms of sections 15 and 16 of this License; or 382 | - b) Requiring preservation of specified reasonable legal notices or 383 | author attributions in that material or in the Appropriate Legal 384 | Notices displayed by works containing it; or 385 | - c) Prohibiting misrepresentation of the origin of that material, 386 | or requiring that modified versions of such material be marked in 387 | reasonable ways as different from the original version; or 388 | - d) Limiting the use for publicity purposes of names of licensors 389 | or authors of the material; or 390 | - e) Declining to grant rights under trademark law for use of some 391 | trade names, trademarks, or service marks; or 392 | - f) Requiring indemnification of licensors and authors of that 393 | material by anyone who conveys the material (or modified versions 394 | of it) with contractual assumptions of liability to the recipient, 395 | for any liability that these contractual assumptions directly 396 | impose on those licensors and authors. 397 | 398 | All other non-permissive additional terms are considered "further 399 | restrictions" within the meaning of section 10. If the Program as you 400 | received it, or any part of it, contains a notice stating that it is 401 | governed by this License along with a term that is a further 402 | restriction, you may remove that term. If a license document contains 403 | a further restriction but permits relicensing or conveying under this 404 | License, you may add to a covered work material governed by the terms 405 | of that license document, provided that the further restriction does 406 | not survive such relicensing or conveying. 407 | 408 | If you add terms to a covered work in accord with this section, you 409 | must place, in the relevant source files, a statement of the 410 | additional terms that apply to those files, or a notice indicating 411 | where to find the applicable terms. 412 | 413 | Additional terms, permissive or non-permissive, may be stated in the 414 | form of a separately written license, or stated as exceptions; the 415 | above requirements apply either way. 416 | 417 | 8. Termination. 418 | ^^^^^^^^^^^^^^^ 419 | 420 | You may not propagate or modify a covered work except as expressly 421 | provided under this License. Any attempt otherwise to propagate or 422 | modify it is void, and will automatically terminate your rights under 423 | this License (including any patent licenses granted under the third 424 | paragraph of section 11). 425 | 426 | However, if you cease all violation of this License, then your license 427 | from a particular copyright holder is reinstated (a) provisionally, 428 | unless and until the copyright holder explicitly and finally 429 | terminates your license, and (b) permanently, if the copyright holder 430 | fails to notify you of the violation by some reasonable means prior to 431 | 60 days after the cessation. 432 | 433 | Moreover, your license from a particular copyright holder is 434 | reinstated permanently if the copyright holder notifies you of the 435 | violation by some reasonable means, this is the first time you have 436 | received notice of violation of this License (for any work) from that 437 | copyright holder, and you cure the violation prior to 30 days after 438 | your receipt of the notice. 439 | 440 | Termination of your rights under this section does not terminate the 441 | licenses of parties who have received copies or rights from you under 442 | this License. If your rights have been terminated and not permanently 443 | reinstated, you do not qualify to receive new licenses for the same 444 | material under section 10. 445 | 446 | 9. Acceptance Not Required for Having Copies. 447 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 448 | 449 | You are not required to accept this License in order to receive or run 450 | a copy of the Program. Ancillary propagation of a covered work 451 | occurring solely as a consequence of using peer-to-peer transmission 452 | to receive a copy likewise does not require acceptance. However, 453 | nothing other than this License grants you permission to propagate or 454 | modify any covered work. These actions infringe copyright if you do 455 | not accept this License. Therefore, by modifying or propagating a 456 | covered work, you indicate your acceptance of this License to do so. 457 | 458 | 10. Automatic Licensing of Downstream Recipients. 459 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 460 | 461 | Each time you convey a covered work, the recipient automatically 462 | receives a license from the original licensors, to run, modify and 463 | propagate that work, subject to this License. You are not responsible 464 | for enforcing compliance by third parties with this License. 465 | 466 | An "entity transaction" is a transaction transferring control of an 467 | organization, or substantially all assets of one, or subdividing an 468 | organization, or merging organizations. If propagation of a covered 469 | work results from an entity transaction, each party to that 470 | transaction who receives a copy of the work also receives whatever 471 | licenses to the work the party's predecessor in interest had or could 472 | give under the previous paragraph, plus a right to possession of the 473 | Corresponding Source of the work from the predecessor in interest, if 474 | the predecessor has it or can get it with reasonable efforts. 475 | 476 | You may not impose any further restrictions on the exercise of the 477 | rights granted or affirmed under this License. For example, you may 478 | not impose a license fee, royalty, or other charge for exercise of 479 | rights granted under this License, and you may not initiate litigation 480 | (including a cross-claim or counterclaim in a lawsuit) alleging that 481 | any patent claim is infringed by making, using, selling, offering for 482 | sale, or importing the Program or any portion of it. 483 | 484 | 11. Patents. 485 | ^^^^^^^^^^^^ 486 | 487 | A "contributor" is a copyright holder who authorizes use under this 488 | License of the Program or a work on which the Program is based. The 489 | work thus licensed is called the contributor's "contributor version". 490 | 491 | A contributor's "essential patent claims" are all patent claims owned 492 | or controlled by the contributor, whether already acquired or 493 | hereafter acquired, that would be infringed by some manner, permitted 494 | by this License, of making, using, or selling its contributor version, 495 | but do not include claims that would be infringed only as a 496 | consequence of further modification of the contributor version. For 497 | purposes of this definition, "control" includes the right to grant 498 | patent sublicenses in a manner consistent with the requirements of 499 | this License. 500 | 501 | Each contributor grants you a non-exclusive, worldwide, royalty-free 502 | patent license under the contributor's essential patent claims, to 503 | make, use, sell, offer for sale, import and otherwise run, modify and 504 | propagate the contents of its contributor version. 505 | 506 | In the following three paragraphs, a "patent license" is any express 507 | agreement or commitment, however denominated, not to enforce a patent 508 | (such as an express permission to practice a patent or covenant not to 509 | sue for patent infringement). To "grant" such a patent license to a 510 | party means to make such an agreement or commitment not to enforce a 511 | patent against the party. 512 | 513 | If you convey a covered work, knowingly relying on a patent license, 514 | and the Corresponding Source of the work is not available for anyone 515 | to copy, free of charge and under the terms of this License, through a 516 | publicly available network server or other readily accessible means, 517 | then you must either (1) cause the Corresponding Source to be so 518 | available, or (2) arrange to deprive yourself of the benefit of the 519 | patent license for this particular work, or (3) arrange, in a manner 520 | consistent with the requirements of this License, to extend the patent 521 | license to downstream recipients. "Knowingly relying" means you have 522 | actual knowledge that, but for the patent license, your conveying the 523 | covered work in a country, or your recipient's use of the covered work 524 | in a country, would infringe one or more identifiable patents in that 525 | country that you have reason to believe are valid. 526 | 527 | If, pursuant to or in connection with a single transaction or 528 | arrangement, you convey, or propagate by procuring conveyance of, a 529 | covered work, and grant a patent license to some of the parties 530 | receiving the covered work authorizing them to use, propagate, modify 531 | or convey a specific copy of the covered work, then the patent license 532 | you grant is automatically extended to all recipients of the covered 533 | work and works based on it. 534 | 535 | A patent license is "discriminatory" if it does not include within the 536 | scope of its coverage, prohibits the exercise of, or is conditioned on 537 | the non-exercise of one or more of the rights that are specifically 538 | granted under this License. You may not convey a covered work if you 539 | are a party to an arrangement with a third party that is in the 540 | business of distributing software, under which you make payment to the 541 | third party based on the extent of your activity of conveying the 542 | work, and under which the third party grants, to any of the parties 543 | who would receive the covered work from you, a discriminatory patent 544 | license (a) in connection with copies of the covered work conveyed by 545 | you (or copies made from those copies), or (b) primarily for and in 546 | connection with specific products or compilations that contain the 547 | covered work, unless you entered into that arrangement, or that patent 548 | license was granted, prior to 28 March 2007. 549 | 550 | Nothing in this License shall be construed as excluding or limiting 551 | any implied license or other defenses to infringement that may 552 | otherwise be available to you under applicable patent law. 553 | 554 | 12. No Surrender of Others' Freedom. 555 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 556 | 557 | If conditions are imposed on you (whether by court order, agreement or 558 | otherwise) that contradict the conditions of this License, they do not 559 | excuse you from the conditions of this License. If you cannot convey a 560 | covered work so as to satisfy simultaneously your obligations under 561 | this License and any other pertinent obligations, then as a 562 | consequence you may not convey it at all. For example, if you agree to 563 | terms that obligate you to collect a royalty for further conveying 564 | from those to whom you convey the Program, the only way you could 565 | satisfy both those terms and this License would be to refrain entirely 566 | from conveying the Program. 567 | 568 | 13. Use with the GNU Affero General Public License. 569 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 570 | 571 | Notwithstanding any other provision of this License, you have 572 | permission to link or combine any covered work with a work licensed 573 | under version 3 of the GNU Affero General Public License into a single 574 | combined work, and to convey the resulting work. The terms of this 575 | License will continue to apply to the part which is the covered work, 576 | but the special requirements of the GNU Affero General Public License, 577 | section 13, concerning interaction through a network will apply to the 578 | combination as such. 579 | 580 | 14. Revised Versions of this License. 581 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 582 | 583 | The Free Software Foundation may publish revised and/or new versions 584 | of the GNU General Public License from time to time. Such new versions 585 | will be similar in spirit to the present version, but may differ in 586 | detail to address new problems or concerns. 587 | 588 | Each version is given a distinguishing version number. If the Program 589 | specifies that a certain numbered version of the GNU General Public 590 | License "or any later version" applies to it, you have the option of 591 | following the terms and conditions either of that numbered version or 592 | of any later version published by the Free Software Foundation. If the 593 | Program does not specify a version number of the GNU General Public 594 | License, you may choose any version ever published by the Free 595 | Software Foundation. 596 | 597 | If the Program specifies that a proxy can decide which future versions 598 | of the GNU General Public License can be used, that proxy's public 599 | statement of acceptance of a version permanently authorizes you to 600 | choose that version for the Program. 601 | 602 | Later license versions may give you additional or different 603 | permissions. However, no additional obligations are imposed on any 604 | author or copyright holder as a result of your choosing to follow a 605 | later version. 606 | 607 | 15. Disclaimer of Warranty. 608 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 609 | 610 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 611 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 612 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 613 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 614 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 615 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 616 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 617 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 618 | CORRECTION. 619 | 620 | 16. Limitation of Liability. 621 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 622 | 623 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 624 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 625 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 626 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 627 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 628 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 629 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 630 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 631 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 632 | 633 | 17. Interpretation of Sections 15 and 16. 634 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 635 | 636 | If the disclaimer of warranty and limitation of liability provided 637 | above cannot be given local legal effect according to their terms, 638 | reviewing courts shall apply local law that most closely approximates 639 | an absolute waiver of all civil liability in connection with the 640 | Program, unless a warranty or assumption of liability accompanies a 641 | copy of the Program in return for a fee. 642 | 643 | END OF TERMS AND CONDITIONS 644 | 645 | How to Apply These Terms to Your New Programs 646 | --------------------------------------------- 647 | 648 | If you develop a new program, and you want it to be of the greatest 649 | possible use to the public, the best way to achieve this is to make it 650 | free software which everyone can redistribute and change under these 651 | terms. 652 | 653 | To do so, attach the following notices to the program. It is safest to 654 | attach them to the start of each source file to most effectively state 655 | the exclusion of warranty; and each file should have at least the 656 | "copyright" line and a pointer to where the full notice is found. 657 | 658 | 659 | Copyright (C) 660 | 661 | This program is free software: you can redistribute it and/or modify 662 | it under the terms of the GNU General Public License as published by 663 | the Free Software Foundation, either version 3 of the License, or 664 | (at your option) any later version. 665 | 666 | This program is distributed in the hope that it will be useful, 667 | but WITHOUT ANY WARRANTY; without even the implied warranty of 668 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 669 | GNU General Public License for more details. 670 | 671 | You should have received a copy of the GNU General Public License 672 | along with this program. If not, see . 673 | 674 | Also add information on how to contact you by electronic and paper 675 | mail. 676 | 677 | If the program does terminal interaction, make it output a short 678 | notice like this when it starts in an interactive mode: 679 | 680 | Copyright (C) 681 | This program comes with ABSOLUTELY NO WARRANTY; for details type \`show w'. 682 | This is free software, and you are welcome to redistribute it 683 | under certain conditions; type \`show c' for details. 684 | 685 | The hypothetical commands \`show w' and \`show c' should show the 686 | appropriate parts of the General Public License. Of course, your 687 | program's commands might be different; for a GUI interface, you would 688 | use an "about box". 689 | 690 | You should also get your employer (if you work as a programmer) or 691 | school, if any, to sign a "copyright disclaimer" for the program, if 692 | necessary. For more information on this, and how to apply and follow 693 | the GNU GPL, see . 694 | 695 | The GNU General Public License does not permit incorporating your 696 | program into proprietary programs. If your program is a subroutine 697 | library, you may consider it more useful to permit linking proprietary 698 | applications with the library. If this is what you want to do, use the 699 | GNU Lesser General Public License instead of this License. But first, 700 | please read . 701 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | rcon 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | rcon 8 | -------------------------------------------------------------------------------- /docs/source/rcon.battleye.rst: -------------------------------------------------------------------------------- 1 | rcon.battleye package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | rcon.battleye.client module 8 | --------------------------- 9 | 10 | .. automodule:: rcon.battleye.client 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | rcon.battleye.proto module 16 | -------------------------- 17 | 18 | .. automodule:: rcon.battleye.proto 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: rcon.battleye 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/source/rcon.rst: -------------------------------------------------------------------------------- 1 | rcon package 2 | ============ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | rcon.battleye 11 | rcon.source 12 | 13 | Submodules 14 | ---------- 15 | 16 | rcon.client module 17 | ------------------ 18 | 19 | .. automodule:: rcon.client 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | rcon.config module 25 | ------------------ 26 | 27 | .. automodule:: rcon.config 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | 32 | rcon.console module 33 | ------------------- 34 | 35 | .. automodule:: rcon.console 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | 40 | rcon.errorhandler module 41 | ------------------------ 42 | 43 | .. automodule:: rcon.errorhandler 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | rcon.exceptions module 49 | ---------------------- 50 | 51 | .. automodule:: rcon.exceptions 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | 56 | rcon.gui module 57 | --------------- 58 | 59 | .. automodule:: rcon.gui 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | 64 | rcon.rconclt module 65 | ------------------- 66 | 67 | .. automodule:: rcon.rconclt 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | 72 | rcon.rconshell module 73 | --------------------- 74 | 75 | .. automodule:: rcon.rconshell 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | rcon.readline module 81 | -------------------- 82 | 83 | .. automodule:: rcon.readline 84 | :members: 85 | :undoc-members: 86 | :show-inheritance: 87 | 88 | Module contents 89 | --------------- 90 | 91 | .. automodule:: rcon 92 | :members: 93 | :undoc-members: 94 | :show-inheritance: 95 | -------------------------------------------------------------------------------- /docs/source/rcon.source.rst: -------------------------------------------------------------------------------- 1 | rcon.source package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | rcon.source.async\_rcon module 8 | ------------------------------ 9 | 10 | .. automodule:: rcon.source.async_rcon 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | rcon.source.client module 16 | ------------------------- 17 | 18 | .. automodule:: rcon.source.client 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | rcon.source.proto module 24 | ------------------------ 25 | 26 | .. automodule:: rcon.source.proto 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: rcon.source 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | Source RCON 4 | ----------- 5 | To connect to a server using the Source RCON protocol, use :py:class:`rcon.source.Client`. 6 | 7 | .. code-block:: python 8 | 9 | from rcon.source import Client 10 | 11 | with Client('127.0.0.1', 5000, passwd='mysecretpassword') as client: 12 | response = client.run('some_command', 'with', 'some', 'arguments') 13 | 14 | print(response) 15 | 16 | BattlEye RCon 17 | ------------- 18 | To connecto to a server using the BattlEye RCon protocol, use :py:class:`rcon.battleye.Client`. 19 | 20 | .. code-block:: python 21 | 22 | from rcon.battleye import Client 23 | 24 | with Client('127.0.0.1', 5000, passwd='mysecretpassword') as client: 25 | response = client.run('some_command', 'with', 'some', 'arguments') 26 | 27 | print(response) 28 | 29 | Handling server messages 30 | ~~~~~~~~~~~~~~~~~~~~~~~~ 31 | Since the BattlEye RCon server will also send server messages to the client 32 | alongside command responses, you can register an event handler to process 33 | those messages: 34 | 35 | .. code-block:: python 36 | 37 | from rcon.battleye import Client, ServerMessage 38 | 39 | def my_message_handler(server_message: ServerMessage) -> None: 40 | """Print server messages.""" 41 | 42 | print('Server message:', server_message) 43 | 44 | with Client( 45 | '127.0.0.1', 46 | 5000, 47 | passwd='mysecretpassword', 48 | message_handler=my_message_handler 49 | ) as client: 50 | response = client.run('some_command', 'with', 'some', 'arguments') 51 | 52 | print('Response:', response) 53 | 54 | Configuration 55 | ------------- 56 | `rconclt` servers can be configured in :file:`/etc/rcon.conf`. 57 | The configuration file format is: 58 | 59 | .. code-block:: ini 60 | 61 | [] 62 | host = 63 | port = 64 | passwd = 65 | 66 | The :code:`passwd` entry is optional. 67 | 68 | rconclt 69 | ------- 70 | `rconclt` is an RCON client script to communicate with game servers via the RCON protocol using the shell. 71 | To communicate with a server, run: 72 | 73 | .. code-block:: bash 74 | 75 | rconclt [options] [...] 76 | 77 | rconshell 78 | --------- 79 | `rconshell` is an interactive RCON console to interact with game servers via the RCON protocol. 80 | To start a shell, run: 81 | 82 | .. code-block:: bash 83 | 84 | rconshell [server] [options] 85 | 86 | Handling connection timeouts. 87 | ----------------------------- 88 | You can specify an optional :code:`timeout=` parameter to allow a connection attempt to time out. 89 | If a timeout is reached during a connection attempt, it will raise a `socket.timeout `_ exception. 90 | The following example will raise a connection timeout after 1.5 seconds: 91 | 92 | .. code-block:: python 93 | 94 | try: 95 | with Client('127.0.0.1', 5000, timeout=1.5) as client: 96 | 97 | except socket.timeout as timeout: 98 | 99 | 100 | .. _configuration: 101 | -------------------------------------------------------------------------------- /rcon/__init__.py: -------------------------------------------------------------------------------- 1 | """RCON client library.""" 2 | 3 | from typing import Any, Coroutine 4 | from warnings import warn 5 | 6 | from rcon.exceptions import EmptyResponse, SessionTimeout, WrongPassword 7 | from rcon.source import rcon as _rcon 8 | from rcon.source import Client as _Client 9 | 10 | 11 | __all__ = ["EmptyResponse", "SessionTimeout", "WrongPassword", "Client", "rcon"] 12 | 13 | 14 | class Client(_Client): 15 | """Wrapper for the rcon.source.Client for backwards compatibility.""" 16 | 17 | def __init__(self, *args, **kwargs): 18 | warn( 19 | "rcon.Client() is deprecated. Use rcon.source.Client() instead.", 20 | DeprecationWarning, 21 | stacklevel=2, 22 | ) 23 | super().__init__(*args, **kwargs) 24 | 25 | 26 | def rcon(*args, **kwargs) -> Coroutine[Any, Any, str]: 27 | """Wrapper for rcon.source.rcon() for backwards compatibility.""" 28 | 29 | warn( 30 | "rcon.rcon() is deprecated. Use rcon.source.rcon() instead.", 31 | DeprecationWarning, 32 | stacklevel=2, 33 | ) 34 | return _rcon(*args, **kwargs) 35 | -------------------------------------------------------------------------------- /rcon/battleye/__init__.py: -------------------------------------------------------------------------------- 1 | """BattlEye RCON implementation.""" 2 | 3 | from rcon.battleye.client import Client 4 | from rcon.battleye.proto import ServerMessage 5 | 6 | 7 | __all__ = ["Client", "ServerMessage"] 8 | -------------------------------------------------------------------------------- /rcon/battleye/client.py: -------------------------------------------------------------------------------- 1 | """BattlEye RCon client.""" 2 | 3 | from logging import getLogger 4 | from socket import SOCK_DGRAM 5 | from typing import Callable 6 | 7 | from rcon.battleye.proto import HEADER_SIZE 8 | from rcon.battleye.proto import RESPONSE_TYPES 9 | from rcon.battleye.proto import CommandRequest 10 | from rcon.battleye.proto import CommandResponse 11 | from rcon.battleye.proto import Header 12 | from rcon.battleye.proto import LoginRequest 13 | from rcon.battleye.proto import LoginResponse 14 | from rcon.battleye.proto import Request 15 | from rcon.battleye.proto import Response 16 | from rcon.battleye.proto import ServerMessage 17 | from rcon.battleye.proto import ServerMessageAck 18 | from rcon.client import BaseClient 19 | from rcon.exceptions import WrongPassword 20 | 21 | 22 | __all__ = ["Client"] 23 | 24 | 25 | MessageHandler = Callable[[ServerMessage], None] 26 | 27 | 28 | def log_message(server_message: ServerMessage) -> None: 29 | """Default handler, logging the server message.""" 30 | 31 | getLogger("Server message").info(server_message.message) 32 | 33 | 34 | class Client(BaseClient, socket_type=SOCK_DGRAM): 35 | """BattlEye RCon client.""" 36 | 37 | def __init__( 38 | self, 39 | *args, 40 | max_length: int = 4096, 41 | message_handler: MessageHandler = log_message, 42 | **kwargs, 43 | ): 44 | super().__init__(*args, **kwargs) 45 | self.max_length = max_length 46 | self.message_handler = message_handler 47 | 48 | def handle_server_message(self, message: ServerMessage) -> None: 49 | """Handle the respective server message.""" 50 | with self._socket.makefile("wb") as file: 51 | file.write(bytes(ServerMessageAck(message.seq))) 52 | 53 | self.message_handler(message) 54 | 55 | def receive(self) -> Response: 56 | """Receive a packet.""" 57 | return RESPONSE_TYPES[ 58 | ( 59 | header := Header.from_bytes( 60 | (data := self._socket.recv(self.max_length))[:HEADER_SIZE] 61 | ) 62 | ).type 63 | ].from_bytes(header, data[HEADER_SIZE:]) 64 | 65 | def receive_transaction(self): 66 | command_responses = [] 67 | login_response = None 68 | seq = 0 69 | 70 | while True: 71 | response = self.receive() 72 | 73 | if isinstance(response, LoginResponse) and login_response is None: 74 | login_response = response 75 | continue 76 | 77 | if isinstance(response, CommandResponse): 78 | command_responses.append(response) 79 | seq = response.seq 80 | if len(command_responses) >= seq: 81 | break 82 | continue 83 | 84 | if isinstance(response, ServerMessage): 85 | self.handle_server_message(response) 86 | 87 | if login_response is not None: 88 | return login_response 89 | 90 | 91 | return "".join( 92 | command_response.message 93 | for command_response in sorted(command_responses, key=lambda cr: cr.seq) 94 | ) 95 | 96 | def communicate(self, request: Request) -> Response | str: 97 | """Send a request and receive a response.""" 98 | with self._socket.makefile("wb") as file: 99 | file.write(bytes(request)) 100 | 101 | return self.receive_transaction() 102 | 103 | def login(self, passwd: str) -> bool: 104 | """Log-in the user.""" 105 | if not self.communicate(LoginRequest(passwd)).success: 106 | raise WrongPassword() 107 | 108 | return True 109 | 110 | def run(self, command: str, *args: str) -> str: 111 | """Execute a command and return the text message.""" 112 | return self.communicate(CommandRequest.from_command(command, *args)) 113 | -------------------------------------------------------------------------------- /rcon/battleye/proto.py: -------------------------------------------------------------------------------- 1 | """Low-level protocol stuff.""" 2 | 3 | from __future__ import annotations 4 | from typing import NamedTuple 5 | from zlib import crc32 6 | 7 | 8 | __all__ = [ 9 | "HEADER_SIZE", 10 | "RESPONSE_TYPES", 11 | "Header", 12 | "LoginRequest", 13 | "LoginResponse", 14 | "CommandRequest", 15 | "CommandResponse", 16 | "ServerMessage", 17 | "ServerMessageAck", 18 | "Request", 19 | "Response", 20 | ] 21 | 22 | 23 | HEADER_SIZE = 8 24 | PREFIX = "BE" 25 | INFIX = 0xFF 26 | 27 | 28 | class Header(NamedTuple): 29 | """Packet header.""" 30 | 31 | crc32: int 32 | type: int 33 | 34 | def __bytes__(self): 35 | return b"".join( 36 | ( 37 | PREFIX.encode("ascii"), 38 | self.crc32.to_bytes(4, "little"), 39 | INFIX.to_bytes(1, "little"), 40 | self.type.to_bytes(1, "little"), 41 | ) 42 | ) 43 | 44 | @classmethod 45 | def create(cls, typ: int, payload: bytes) -> Header: 46 | """Create a header for the given payload.""" 47 | return cls( 48 | crc32( 49 | b"".join( 50 | (INFIX.to_bytes(1, "little"), typ.to_bytes(1, "little"), payload) 51 | ) 52 | ), 53 | typ, 54 | ) 55 | 56 | @classmethod 57 | def from_bytes(cls, payload: bytes) -> Header: 58 | """Create a header from the given bytes.""" 59 | if (size := len(payload)) != HEADER_SIZE: 60 | raise ValueError("Invalid payload size", size) 61 | 62 | if (prefix := payload[:2].decode("ascii")) != PREFIX: 63 | raise ValueError("Invalid prefix", prefix) 64 | 65 | if (infix := int.from_bytes(payload[6:7], "little")) != INFIX: 66 | raise ValueError("Invalid infix", infix) 67 | 68 | return cls( 69 | int.from_bytes(payload[2:6], "little"), 70 | int.from_bytes(payload[7:8], "little"), 71 | ) 72 | 73 | 74 | class LoginRequest(str): 75 | """Login request packet.""" 76 | 77 | def __bytes__(self): 78 | return bytes(self.header) + self.payload 79 | 80 | @property 81 | def payload(self) -> bytes: 82 | """Return the payload.""" 83 | return self.encode("ascii") 84 | 85 | @property 86 | def header(self) -> Header: 87 | """Return the appropriate header.""" 88 | return Header.create(0x00, self.payload) 89 | 90 | 91 | class LoginResponse(NamedTuple): 92 | """A login response.""" 93 | 94 | header: Header 95 | success: bool 96 | 97 | @classmethod 98 | def from_bytes(cls, header: Header, payload: bytes) -> LoginResponse: 99 | """Create a login response from the given bytes.""" 100 | return cls(header, bool(int.from_bytes(payload[:1], "little"))) 101 | 102 | 103 | class CommandRequest(NamedTuple): 104 | """Command packet.""" 105 | 106 | seq: int 107 | command: str 108 | 109 | def __bytes__(self): 110 | return bytes(self.header) + self.payload 111 | 112 | @property 113 | def payload(self) -> bytes: 114 | """Return the payload.""" 115 | return b"".join((self.seq.to_bytes(1, "little"), self.command.encode("ascii"))) 116 | 117 | @property 118 | def header(self) -> Header: 119 | """Return the appropriate header.""" 120 | return Header.create(0x01, self.payload) 121 | 122 | @classmethod 123 | def from_string(cls, command: str) -> CommandRequest: 124 | """Create a command packet from the given string.""" 125 | return cls(0x00, command) 126 | 127 | @classmethod 128 | def from_command(cls, command: str, *args: str) -> CommandRequest: 129 | """Create a command packet from the command and arguments.""" 130 | return cls.from_string(" ".join([command, *args])) 131 | 132 | 133 | class CommandResponse(NamedTuple): 134 | """A command response.""" 135 | 136 | header: Header 137 | seq: int 138 | payload: bytes 139 | 140 | @classmethod 141 | def from_bytes(cls, header: Header, payload: bytes) -> CommandResponse: 142 | """Create a command response from the given bytes.""" 143 | return cls(header, int.from_bytes(payload[:1], "little"), payload[1:]) 144 | 145 | @property 146 | def message(self) -> str: 147 | """Return the text message.""" 148 | return self.payload.decode("ascii") 149 | 150 | 151 | class ServerMessage(NamedTuple): 152 | """A message from the server.""" 153 | 154 | header: Header 155 | seq: int 156 | payload: bytes 157 | 158 | @classmethod 159 | def from_bytes(cls, header: Header, payload: bytes) -> ServerMessage: 160 | """Create a server message from the given bytes.""" 161 | return cls(header, int.from_bytes(payload[:1], "little"), payload[1:]) 162 | 163 | @property 164 | def message(self) -> str: 165 | """Return the text message.""" 166 | return self.payload.decode("ascii") 167 | 168 | 169 | class ServerMessageAck(NamedTuple): 170 | """An acknowledgement of a message from the server.""" 171 | 172 | seq: int 173 | 174 | def __bytes__(self): 175 | return bytes(self.header) + self.payload 176 | 177 | @property 178 | def header(self) -> Header: 179 | """Return the appropriate header.""" 180 | return Header.create(0x02, self.payload) 181 | 182 | @property 183 | def payload(self) -> bytes: 184 | """Return the payload.""" 185 | return self.seq.to_bytes(1, "little") 186 | 187 | 188 | Request = LoginRequest | CommandRequest | ServerMessageAck 189 | Response = LoginResponse | CommandResponse | ServerMessage 190 | 191 | RESPONSE_TYPES = {0x00: LoginResponse, 0x01: CommandResponse, 0x02: ServerMessage} 192 | -------------------------------------------------------------------------------- /rcon/client.py: -------------------------------------------------------------------------------- 1 | """Common base client.""" 2 | 3 | from socket import SocketKind, socket 4 | 5 | 6 | __all__ = ["BaseClient"] 7 | 8 | 9 | class BaseClient: 10 | """A common RCON client.""" 11 | 12 | def __init__( 13 | self, 14 | host: str, 15 | port: int, 16 | *, 17 | timeout: float | None = None, 18 | passwd: str | None = None 19 | ): 20 | """Initialize the base client.""" 21 | self._socket = socket(type=self._socket_type) 22 | self.host = host 23 | self.port = port 24 | self.timeout = timeout 25 | self.passwd = passwd 26 | 27 | def __init_subclass__(cls, *, socket_type: SocketKind | None = None): 28 | if socket_type is not None: 29 | cls._socket_type = socket_type 30 | 31 | def __enter__(self): 32 | """Attempt an auto-login if a password is set.""" 33 | self._socket.__enter__() 34 | self.connect(login=True) 35 | return self 36 | 37 | def __exit__(self, typ, value, traceback): 38 | """Delegate to the underlying socket's exit method.""" 39 | return self._socket.__exit__(typ, value, traceback) 40 | 41 | @property 42 | def timeout(self) -> float: 43 | """Return the socket timeout.""" 44 | return self._socket.gettimeout() 45 | 46 | @timeout.setter 47 | def timeout(self, timeout: float): 48 | """Set the socket timeout.""" 49 | self._socket.settimeout(timeout) 50 | 51 | def connect(self, login: bool = False) -> None: 52 | """Connect the socket and attempt a 53 | login if wanted and a password is set. 54 | """ 55 | self._socket.connect((self.host, self.port)) 56 | 57 | if login and self.passwd is not None: 58 | self.login(self.passwd) 59 | 60 | def close(self) -> None: 61 | """Close the socket connection.""" 62 | self._socket.close() 63 | 64 | def login(self, passwd: str) -> bool: 65 | """Perform a login.""" 66 | raise NotImplementedError() 67 | 68 | def run(self, command: str, *args: str) -> str: 69 | """Run a command.""" 70 | raise NotImplementedError() 71 | -------------------------------------------------------------------------------- /rcon/config.py: -------------------------------------------------------------------------------- 1 | """RCON server configuration.""" 2 | 3 | from __future__ import annotations 4 | from argparse import Namespace 5 | from configparser import ConfigParser, SectionProxy 6 | from getpass import getpass 7 | from logging import getLogger 8 | from os import getenv, name 9 | from pathlib import Path 10 | from typing import Iterable, NamedTuple 11 | 12 | from rcon.exceptions import ConfigReadError, UserAbort 13 | 14 | 15 | __all__ = ["CONFIG_FILES", "LOG_FORMAT", "SERVERS", "Config", "from_args"] 16 | 17 | 18 | CONFIG = ConfigParser() 19 | 20 | if name == "posix": 21 | CONFIG_FILES = ( 22 | Path("/etc/rcon.conf"), 23 | Path("/usr/local/etc/rcon.conf"), 24 | Path.home().joinpath(".rcon.conf"), 25 | ) 26 | elif name == "nt": 27 | CONFIG_FILES = ( 28 | Path(getenv("LOCALAPPDATA")).joinpath("rcon.conf"), 29 | Path.home().joinpath(".rcon.conf"), 30 | ) 31 | else: 32 | raise NotImplementedError(f"Unsupported operating system: {name}") 33 | 34 | LOG_FORMAT = "[%(levelname)s] %(name)s: %(message)s" 35 | LOGGER = getLogger("RCON Config") 36 | SERVERS = {} 37 | 38 | 39 | class Config(NamedTuple): 40 | """Represents server configuration.""" 41 | 42 | host: str 43 | port: int 44 | passwd: str | None = None 45 | 46 | @classmethod 47 | def from_string(cls, string: str) -> Config: 48 | """Read the credentials from the given string.""" 49 | try: 50 | host, port = string.rsplit(":", maxsplit=1) 51 | except ValueError: 52 | raise ValueError(f"Invalid socket: {string}.") from None 53 | 54 | port = int(port) 55 | 56 | try: 57 | passwd, host = host.rsplit("@", maxsplit=1) 58 | except ValueError: 59 | passwd = None 60 | 61 | return cls(host, port, passwd) 62 | 63 | @classmethod 64 | def from_config_section(cls, section: SectionProxy) -> Config: 65 | """Create a credentials tuple from 66 | the respective config section. 67 | """ 68 | host = section["host"] 69 | port = section.getint("port") 70 | passwd = section.get("passwd") 71 | return cls(host, port, passwd) 72 | 73 | 74 | def load(config_files: Path | Iterable[Path] = CONFIG_FILES) -> None: 75 | """Read the configuration files and populates SERVERS.""" 76 | 77 | SERVERS.clear() 78 | CONFIG.read(config_files) 79 | 80 | for section in CONFIG.sections(): 81 | SERVERS[section] = Config.from_config_section(CONFIG[section]) 82 | 83 | 84 | def from_args(args: Namespace) -> Config: 85 | """Get the credentials for a server from the respective arguments.""" 86 | 87 | try: 88 | host, port, passwd = Config.from_string(args.server) 89 | except ValueError: 90 | load(args.config) 91 | 92 | try: 93 | host, port, passwd = SERVERS[args.server] 94 | except KeyError: 95 | LOGGER.error("No such server: %s.", args.server) 96 | raise ConfigReadError() from None 97 | 98 | if passwd is None: 99 | try: 100 | passwd = getpass("Password: ") 101 | except (KeyboardInterrupt, EOFError): 102 | print() 103 | LOGGER.error("Aborted by user.") 104 | raise UserAbort() from None 105 | 106 | return Config(host, port, passwd) 107 | -------------------------------------------------------------------------------- /rcon/console.py: -------------------------------------------------------------------------------- 1 | """An interactive console.""" 2 | 3 | from getpass import getpass 4 | from typing import Type 5 | 6 | from rcon.client import BaseClient 7 | from rcon.config import Config 8 | from rcon.exceptions import EmptyResponse, SessionTimeout, WrongPassword 9 | 10 | 11 | __all__ = ["PROMPT", "rconcmd"] 12 | 13 | 14 | EXIT_COMMANDS = {"exit", "quit"} 15 | MSG_LOGIN_ABORTED = "\nLogin aborted. Bye." 16 | MSG_EXIT = "\nBye." 17 | MSG_SERVER_GONE = "Server has gone away." 18 | MSG_SESSION_TIMEOUT = "Session timed out. Please login again." 19 | PROMPT = "RCON {host}:{port}> " 20 | VALID_PORTS = range(0, 65536) 21 | 22 | 23 | def read_host() -> str: 24 | """Read the host.""" 25 | 26 | while True: 27 | try: 28 | return input("Host: ") 29 | except KeyboardInterrupt: 30 | print() 31 | continue 32 | 33 | 34 | def read_port() -> int: 35 | """Read the port.""" 36 | 37 | while True: 38 | try: 39 | port = input("Port: ") 40 | except KeyboardInterrupt: 41 | print() 42 | continue 43 | 44 | try: 45 | port = int(port) 46 | except ValueError: 47 | print(f"Invalid integer: {port}") 48 | continue 49 | 50 | if port in VALID_PORTS: 51 | return port 52 | 53 | print(f"Invalid port: {port}") 54 | 55 | 56 | def read_passwd() -> str: 57 | """Read the password.""" 58 | 59 | while True: 60 | try: 61 | return getpass("Password: ") 62 | except KeyboardInterrupt: 63 | print() 64 | 65 | 66 | def get_config(host: str, port: int, passwd: str) -> Config: 67 | """Read the necessary arguments.""" 68 | 69 | if host is None: 70 | host = read_host() 71 | 72 | if port is None: 73 | port = read_port() 74 | 75 | if passwd is None: 76 | passwd = read_passwd() 77 | 78 | return Config(host, port, passwd) 79 | 80 | 81 | def login(client: BaseClient, passwd: str) -> str: 82 | """Perform a login.""" 83 | 84 | while True: 85 | try: 86 | client.login(passwd) 87 | except WrongPassword: 88 | print("Wrong password.") 89 | passwd = read_passwd() 90 | continue 91 | 92 | return passwd 93 | 94 | 95 | def process_input(client: BaseClient, passwd: str, prompt: str) -> bool: 96 | """Process the CLI input.""" 97 | 98 | try: 99 | command = input(prompt) 100 | except KeyboardInterrupt: 101 | print() 102 | return True 103 | except EOFError: 104 | print(MSG_EXIT) 105 | return False 106 | 107 | try: 108 | command, *args = command.split() 109 | except ValueError: 110 | return True 111 | 112 | if command in EXIT_COMMANDS: 113 | return False 114 | 115 | try: 116 | result = client.run(command, *args) 117 | except EmptyResponse: 118 | print(MSG_SERVER_GONE) 119 | return False 120 | except SessionTimeout: 121 | print(MSG_SESSION_TIMEOUT) 122 | 123 | try: 124 | login(client, passwd) 125 | except EOFError: 126 | print(MSG_LOGIN_ABORTED) 127 | return False 128 | 129 | return True 130 | 131 | if result: 132 | print(result) 133 | 134 | return True 135 | 136 | 137 | def rconcmd( 138 | client_cls: Type[BaseClient], 139 | host: str, 140 | port: int, 141 | passwd: str, 142 | *, 143 | timeout: float | None = None, 144 | prompt: str = PROMPT, 145 | ): 146 | """Initialize the console.""" 147 | 148 | try: 149 | host, port, passwd = get_config(host, port, passwd) 150 | except EOFError: 151 | print(MSG_EXIT) 152 | return 153 | 154 | prompt = prompt.format(host=host, port=port) 155 | 156 | with client_cls(host, port, timeout=timeout) as client: 157 | try: 158 | passwd = login(client, passwd) 159 | except EOFError: 160 | print(MSG_LOGIN_ABORTED) 161 | return 162 | 163 | while True: 164 | if not process_input(client, passwd, prompt): 165 | break 166 | -------------------------------------------------------------------------------- /rcon/errorhandler.py: -------------------------------------------------------------------------------- 1 | """Common errors handler.""" 2 | 3 | from logging import Logger 4 | from socket import timeout 5 | 6 | from rcon.exceptions import ConfigReadError 7 | from rcon.exceptions import SessionTimeout 8 | from rcon.exceptions import UserAbort 9 | from rcon.exceptions import WrongPassword 10 | 11 | 12 | __all__ = ["ErrorHandler"] 13 | 14 | 15 | ERRORS = { 16 | UserAbort: (1, None), 17 | ConfigReadError: (2, None), 18 | ConnectionRefusedError: (3, "Connection refused."), 19 | (TimeoutError, timeout): (4, "Connection timed out."), 20 | WrongPassword: (5, "Wrong password."), 21 | SessionTimeout: (6, "Session timed out."), 22 | } 23 | 24 | 25 | class ErrorHandler: 26 | """Handles common errors and exits.""" 27 | 28 | __slots__ = ("logger", "exit_code") 29 | 30 | def __init__(self, logger: Logger): 31 | """Set the logger.""" 32 | self.logger = logger 33 | self.exit_code = 0 34 | 35 | def __enter__(self): 36 | return self 37 | 38 | def __exit__(self, _, value: Exception, __): 39 | """Check for common errors and exit respectively.""" 40 | if value is None: 41 | return True 42 | 43 | for typ, (exit_code, message) in ERRORS.items(): 44 | if isinstance(value, typ): 45 | self.exit_code = exit_code 46 | 47 | if message: 48 | self.logger.error(message) 49 | 50 | return True 51 | 52 | return None 53 | -------------------------------------------------------------------------------- /rcon/exceptions.py: -------------------------------------------------------------------------------- 1 | """Common exceptions.""" 2 | 3 | __all__ = [ 4 | "ConfigReadError", 5 | "EmptyResponse", 6 | "SessionTimeout", 7 | "UserAbort", 8 | "WrongPassword", 9 | ] 10 | 11 | 12 | class ConfigReadError(Exception): 13 | """Indicates an error while reading the configuration.""" 14 | 15 | 16 | class EmptyResponse(Exception): 17 | """Indicates an empty response from the server.""" 18 | 19 | 20 | class SessionTimeout(Exception): 21 | """Indicates that the session timed out.""" 22 | 23 | 24 | class UserAbort(Exception): 25 | """Indicates that a required action has been aborted by the user.""" 26 | 27 | 28 | class WrongPassword(Exception): 29 | """Indicates a wrong password.""" 30 | 31 | 32 | class UnexpectedTerminator(Exception): 33 | """Indicates an unexpected terminator in the response.""" 34 | -------------------------------------------------------------------------------- /rcon/gui.py: -------------------------------------------------------------------------------- 1 | """GTK based GUI.""" 2 | 3 | from argparse import ArgumentParser, Namespace 4 | from json import dump, load 5 | from logging import DEBUG, INFO, basicConfig, getLogger 6 | from os import getenv, name 7 | from pathlib import Path 8 | from socket import gaierror, timeout 9 | from typing import Iterable, NamedTuple, Type 10 | 11 | from gi import require_version 12 | 13 | require_version("Gtk", "3.0") 14 | from gi.repository import Gtk 15 | 16 | from rcon import battleye, source 17 | from rcon.client import BaseClient 18 | from rcon.config import LOG_FORMAT 19 | from rcon.exceptions import SessionTimeout, WrongPassword 20 | 21 | 22 | __all__ = ["main"] 23 | 24 | 25 | if name == "posix": 26 | CACHE_DIR = Path.home().joinpath(".cache") 27 | elif name == "nt": 28 | CACHE_DIR = Path(getenv("TEMP") or getenv("TMP")) 29 | else: 30 | raise NotImplementedError("Unsupported operating system.") 31 | 32 | 33 | CACHE_FILE = CACHE_DIR.joinpath("rcongui.json") 34 | LOGGER = getLogger("rcongui") 35 | 36 | 37 | def get_args() -> Namespace: 38 | """Parse and return the command line arguments.""" 39 | 40 | parser = ArgumentParser(description="A minimalistic, GTK-based RCON GUI.") 41 | parser.add_argument( 42 | "-B", 43 | "--battleye", 44 | action="store_true", 45 | help="use BattlEye RCon instead of Source RCON", 46 | ) 47 | parser.add_argument( 48 | "-d", "--debug", action="store_true", help="print additional debug information" 49 | ) 50 | parser.add_argument( 51 | "-t", 52 | "--timeout", 53 | type=float, 54 | metavar="seconds", 55 | help="connection timeout in seconds", 56 | ) 57 | return parser.parse_args() 58 | 59 | 60 | class RCONParams(NamedTuple): 61 | """Represent the RCON parameters.""" 62 | 63 | host: str 64 | port: int 65 | passwd: str 66 | command: Iterable[str] 67 | 68 | 69 | class GUI(Gtk.Window): 70 | """A GTK based GUI for RCON.""" 71 | 72 | def __init__(self, args: Namespace): 73 | """Initialize the GUI.""" 74 | super().__init__(title="RCON GUI") 75 | self.args = args 76 | 77 | self.set_position(Gtk.WindowPosition.CENTER) 78 | 79 | self.grid = Gtk.Grid() 80 | self.add(self.grid) 81 | 82 | self.host = Gtk.Entry() 83 | self.host.set_placeholder_text("Host") 84 | self.grid.attach(self.host, 0, 0, 1, 1) 85 | 86 | self.port = Gtk.SpinButton.new_with_range(0, 65535, 1) 87 | self.port.set_placeholder_text("Port") 88 | self.grid.attach(self.port, 1, 0, 1, 1) 89 | 90 | self.passwd = Gtk.Entry() 91 | self.passwd.set_placeholder_text("Password") 92 | self.passwd.set_visibility(False) 93 | self.grid.attach(self.passwd, 2, 0, 1, 1) 94 | 95 | self.command = Gtk.Entry() 96 | self.command.set_placeholder_text("Command") 97 | self.grid.attach(self.command, 0, 1, 2, 1) 98 | 99 | self.button = Gtk.Button(label="Run") 100 | self.button.connect("clicked", self.on_button_clicked) 101 | self.grid.attach(self.button, 2, 1, 1, 1) 102 | 103 | self.result = Gtk.TextView() 104 | self.result.set_wrap_mode(Gtk.WrapMode.WORD) 105 | self.result.set_property("editable", False) 106 | self.grid.attach(self.result, 0, 2, 2, 1) 107 | 108 | self.savepw = Gtk.CheckButton(label="Save password") 109 | self.grid.attach(self.savepw, 2, 2, 1, 1) 110 | 111 | self.load_gui_settings() 112 | 113 | @property 114 | def client_cls(self) -> Type[BaseClient]: 115 | """Return the client class.""" 116 | return battleye.Client if self.args.battleye else source.Client 117 | 118 | @property 119 | def result_text(self) -> str: 120 | """Return the result text.""" 121 | if (buf := self.result.get_buffer()) is not None: 122 | return buf.get_text( 123 | buf.get_iter_at_line(0), 124 | buf.get_iter_at_line(buf.get_line_count()), 125 | True, 126 | ) 127 | 128 | return "" 129 | 130 | @result_text.setter 131 | def result_text(self, text: str): 132 | """Set the result text.""" 133 | if (buf := self.result.get_buffer()) is not None: 134 | buf.set_text(text) 135 | 136 | @property 137 | def gui_settings(self) -> dict: 138 | """Return the GUI settings as a dict.""" 139 | json = { 140 | "host": self.host.get_text(), 141 | "port": self.port.get_value_as_int(), 142 | "command": self.command.get_text(), 143 | "result": self.result_text, 144 | "savepw": (savepw := self.savepw.get_active()), 145 | } 146 | 147 | if savepw: 148 | json["passwd"] = self.passwd.get_text() 149 | 150 | return json 151 | 152 | @gui_settings.setter 153 | def gui_settings(self, json: dict): 154 | """Set the GUI settings.""" 155 | self.host.set_text(json.get("host", "")) 156 | self.port.set_value(json.get("port", 0)) 157 | self.passwd.set_text(json.get("passwd", "")) 158 | self.command.set_text(json.get("command", "")) 159 | self.result_text = json.get("result", "") 160 | self.savepw.set_active(json.get("savepw", False)) 161 | 162 | def load_gui_settings(self) -> None: 163 | """Load the GUI settings from the cache file.""" 164 | try: 165 | with CACHE_FILE.open("rb") as cache: 166 | self.gui_settings = load(cache) 167 | except FileNotFoundError: 168 | LOGGER.warning("Cache file not found: %s", CACHE_FILE) 169 | except PermissionError: 170 | LOGGER.error("Insufficient permissions to read: %s", CACHE_FILE) 171 | except ValueError: 172 | LOGGER.error("Cache file contains garbage: %s", CACHE_FILE) 173 | 174 | def save_gui_settings(self): 175 | """Save the GUI settings to the cache file.""" 176 | try: 177 | with CACHE_FILE.open("w", encoding="utf-8") as cache: 178 | dump(self.gui_settings, cache, indent=2) 179 | except PermissionError: 180 | LOGGER.error("Insufficient permissions to read: %s", CACHE_FILE) 181 | 182 | def show_error(self, message: str): 183 | """Show an error message.""" 184 | message_dialog = Gtk.MessageDialog( 185 | transient_for=self, 186 | message_type=Gtk.MessageType.ERROR, 187 | buttons=Gtk.ButtonsType.OK, 188 | text=message, 189 | ) 190 | message_dialog.run() 191 | message_dialog.destroy() 192 | 193 | def run_rcon(self) -> str: 194 | """Return the current RCON settings.""" 195 | with self.client_cls( 196 | self.host.get_text().strip(), 197 | self.port.get_value_as_int(), 198 | timeout=self.args.timeout, 199 | passwd=self.passwd.get_text(), 200 | ) as client: 201 | return client.run(*self.command.get_text().strip().split()) 202 | 203 | def on_button_clicked(self, _): 204 | """Run the client.""" 205 | try: 206 | result = self.run_rcon() 207 | except ValueError as error: 208 | self.show_error(str(error)) 209 | except gaierror as error: 210 | self.show_error(error.strerror) 211 | except ConnectionRefusedError: 212 | self.show_error("Connection refused.") 213 | except (TimeoutError, timeout): 214 | self.show_error("Connection timed out.") 215 | except WrongPassword: 216 | self.show_error("Wrong password.") 217 | except SessionTimeout: 218 | self.show_error("Session timed out.") 219 | else: 220 | self.result_text = result 221 | 222 | def terminate(self, *args, **kwargs): 223 | """Save the settings and terminates the application.""" 224 | self.save_gui_settings() 225 | Gtk.main_quit(*args, **kwargs) 226 | 227 | 228 | def main() -> None: 229 | """Start the GUI.""" 230 | 231 | args = get_args() 232 | basicConfig(format=LOG_FORMAT, level=DEBUG if args.debug else INFO) 233 | win = GUI(args) 234 | win.connect("destroy", win.terminate) 235 | win.show_all() 236 | Gtk.main() 237 | -------------------------------------------------------------------------------- /rcon/rconclt.py: -------------------------------------------------------------------------------- 1 | """RCON client CLI.""" 2 | 3 | from argparse import ArgumentParser, Namespace 4 | from logging import DEBUG, INFO, basicConfig, getLogger 5 | from pathlib import Path 6 | 7 | from rcon import battleye, source 8 | from rcon.config import CONFIG_FILES, LOG_FORMAT, from_args 9 | from rcon.errorhandler import ErrorHandler 10 | 11 | 12 | __all__ = ["main"] 13 | 14 | 15 | LOGGER = getLogger("rconclt") 16 | 17 | 18 | def get_args() -> Namespace: 19 | """Parse and return the command line arguments.""" 20 | 21 | parser = ArgumentParser(description="A Minecraft RCON client.") 22 | parser.add_argument("server", help="the server to connect to") 23 | parser.add_argument( 24 | "-B", 25 | "--battleye", 26 | action="store_true", 27 | help="use BattlEye RCon instead of Source RCON", 28 | ) 29 | parser.add_argument( 30 | "-c", 31 | "--config", 32 | type=Path, 33 | metavar="file", 34 | default=CONFIG_FILES, 35 | help="the configuration file", 36 | ) 37 | parser.add_argument( 38 | "-d", "--debug", action="store_true", help="print additional debug information" 39 | ) 40 | parser.add_argument( 41 | "-t", 42 | "--timeout", 43 | type=float, 44 | metavar="seconds", 45 | help="connection timeout in seconds", 46 | ) 47 | parser.add_argument("command", help="command to execute on the server") 48 | parser.add_argument( 49 | "argument", nargs="*", default=(), help="arguments for the command" 50 | ) 51 | return parser.parse_args() 52 | 53 | 54 | def run() -> None: 55 | """Run the RCON client.""" 56 | 57 | args = get_args() 58 | basicConfig(format=LOG_FORMAT, level=DEBUG if args.debug else INFO) 59 | host, port, passwd = from_args(args) 60 | client_cls = battleye.Client if args.battleye else source.Client 61 | 62 | with client_cls(host, port, timeout=args.timeout) as client: 63 | client.login(passwd) 64 | 65 | if text := client.run(args.command, *args.argument): 66 | print(text, flush=True) 67 | 68 | 69 | def main() -> int: 70 | """Run the main script with exceptions handled.""" 71 | 72 | with ErrorHandler(LOGGER) as handler: 73 | run() 74 | 75 | return handler.exit_code 76 | -------------------------------------------------------------------------------- /rcon/rconshell.py: -------------------------------------------------------------------------------- 1 | """An interactive RCON shell.""" 2 | 3 | from argparse import ArgumentParser, Namespace 4 | from logging import INFO, basicConfig, getLogger 5 | from pathlib import Path 6 | 7 | from rcon import battleye, source 8 | from rcon.readline import CommandHistory 9 | from rcon.config import CONFIG_FILES, LOG_FORMAT, from_args 10 | from rcon.console import PROMPT, rconcmd 11 | from rcon.errorhandler import ErrorHandler 12 | 13 | 14 | __all__ = ["get_args", "main"] 15 | 16 | 17 | LOGGER = getLogger("rconshell") 18 | 19 | 20 | def get_args() -> Namespace: 21 | """Parse and returns the CLI arguments.""" 22 | 23 | parser = ArgumentParser(description="An interactive RCON shell.") 24 | parser.add_argument("server", nargs="?", help="the server to connect to") 25 | parser.add_argument( 26 | "-B", 27 | "--battleye", 28 | action="store_true", 29 | help="use BattlEye RCon instead of Source RCON", 30 | ) 31 | parser.add_argument( 32 | "-c", 33 | "--config", 34 | type=Path, 35 | metavar="file", 36 | default=CONFIG_FILES, 37 | help="the configuration file", 38 | ) 39 | parser.add_argument( 40 | "-p", "--prompt", default=PROMPT, metavar="PS1", help="the shell prompt" 41 | ) 42 | parser.add_argument( 43 | "-t", 44 | "--timeout", 45 | type=float, 46 | metavar="seconds", 47 | help="connection timeout in seconds", 48 | ) 49 | return parser.parse_args() 50 | 51 | 52 | def run() -> None: 53 | """Run the RCON shell.""" 54 | 55 | args = get_args() 56 | basicConfig(level=INFO, format=LOG_FORMAT) 57 | client_cls = battleye.Client if args.battleye else source.Client 58 | 59 | if args.server: 60 | host, port, passwd = from_args(args) 61 | else: 62 | host = port = passwd = None 63 | 64 | with CommandHistory(LOGGER): 65 | rconcmd( 66 | client_cls, host, port, passwd, timeout=args.timeout, prompt=args.prompt 67 | ) 68 | 69 | 70 | def main() -> int: 71 | """Run the main script with exceptions handled.""" 72 | 73 | with ErrorHandler(LOGGER) as handler: 74 | run() 75 | 76 | return handler.exit_code 77 | -------------------------------------------------------------------------------- /rcon/readline.py: -------------------------------------------------------------------------------- 1 | """Wrapper for readline support.""" 2 | 3 | from logging import Logger 4 | from pathlib import Path 5 | 6 | try: 7 | from readline import read_history_file, write_history_file 8 | except ModuleNotFoundError: 9 | read_history_file = write_history_file = lambda _: None 10 | 11 | 12 | __all__ = ["CommandHistory"] 13 | 14 | 15 | HIST_FILE = Path.home() / ".rconshell_history" 16 | 17 | 18 | class CommandHistory: 19 | """Context manager for the command line history.""" 20 | 21 | def __init__(self, logger: Logger, file: Path = HIST_FILE): 22 | """Set the logger to use.""" 23 | self.logger = logger 24 | self.file = file 25 | 26 | def __enter__(self): 27 | """Load the history file.""" 28 | try: 29 | read_history_file(self.file) 30 | except FileNotFoundError: 31 | self.logger.warning("Could not find history file: %s", self.file) 32 | except PermissionError: 33 | self.logger.error("Insufficient permissions to read: %s", self.file) 34 | 35 | return self 36 | 37 | def __exit__(self, *_): 38 | """Write to the history file.""" 39 | try: 40 | write_history_file(self.file) 41 | except PermissionError: 42 | self.logger.error("Insufficient permissions to write: %s", self.file) 43 | -------------------------------------------------------------------------------- /rcon/source/__init__.py: -------------------------------------------------------------------------------- 1 | """Source RCON implementation.""" 2 | 3 | from rcon.source.async_rcon import rcon 4 | from rcon.source.client import Client 5 | 6 | 7 | __all__ = ["Client", "rcon"] 8 | -------------------------------------------------------------------------------- /rcon/source/async_rcon.py: -------------------------------------------------------------------------------- 1 | """Asynchronous RCON.""" 2 | 3 | from asyncio import StreamReader, StreamWriter, open_connection, wait_for 4 | 5 | from rcon.exceptions import SessionTimeout, WrongPassword 6 | from rcon.source.proto import Packet, Type 7 | 8 | 9 | __all__ = ["rcon"] 10 | 11 | 12 | async def close(writer: StreamWriter) -> None: 13 | """Close socket asynchronously.""" 14 | 15 | writer.close() 16 | await writer.wait_closed() 17 | 18 | 19 | async def communicate( 20 | reader: StreamReader, 21 | writer: StreamWriter, 22 | packet: Packet, 23 | *, 24 | frag_threshold: int = 4096, 25 | frag_detect_cmd: str = "", 26 | raise_unexpected_terminator: bool = False, 27 | ) -> Packet: 28 | """Make an asynchronous request.""" 29 | 30 | writer.write(bytes(packet)) 31 | await writer.drain() 32 | response = await Packet.aread(reader, raise_unexpected_terminator) 33 | 34 | if len(response.payload) < frag_threshold: 35 | return response 36 | 37 | writer.write(bytes(Packet.make_command(frag_detect_cmd))) 38 | await writer.drain() 39 | 40 | while ( 41 | successor := await Packet.aread(reader, raise_unexpected_terminator) 42 | ).id == response.id: 43 | response += successor 44 | 45 | return response 46 | 47 | 48 | async def rcon( 49 | command: str, 50 | *arguments: str, 51 | host: str, 52 | port: int, 53 | passwd: str, 54 | encoding: str = "utf-8", 55 | frag_threshold: int = 4096, 56 | frag_detect_cmd: str = "", 57 | timeout: int | None = None, 58 | enforce_id: bool = True, 59 | raise_unexpected_terminator: bool = False, 60 | ) -> str: 61 | """Run a command asynchronously.""" 62 | 63 | reader, writer = await wait_for(open_connection(host, port), timeout=timeout) 64 | response = await communicate( 65 | reader, 66 | writer, 67 | Packet.make_login(passwd, encoding=encoding), 68 | frag_threshold=frag_threshold, 69 | frag_detect_cmd=frag_detect_cmd, 70 | ) 71 | 72 | # Wait for SERVERDATA_AUTH_RESPONSE according to: 73 | # https://developer.valvesoftware.com/wiki/Source_RCON_Protocol 74 | while response.type != Type.SERVERDATA_AUTH_RESPONSE: 75 | response = await Packet.aread(reader, raise_unexpected_terminator) 76 | 77 | if response.id == -1: 78 | await close(writer) 79 | raise WrongPassword() 80 | 81 | request = Packet.make_command(command, *arguments, encoding=encoding) 82 | response = await communicate( 83 | reader, writer, request, raise_unexpected_terminator=raise_unexpected_terminator 84 | ) 85 | await close(writer) 86 | 87 | if enforce_id and response.id != request.id: 88 | raise SessionTimeout() 89 | 90 | return response.payload.decode(encoding) 91 | -------------------------------------------------------------------------------- /rcon/source/client.py: -------------------------------------------------------------------------------- 1 | """Synchronous client.""" 2 | 3 | from socket import SOCK_STREAM 4 | 5 | from rcon.client import BaseClient 6 | from rcon.exceptions import SessionTimeout, WrongPassword 7 | from rcon.source.proto import Packet, Type 8 | 9 | __all__ = ["Client"] 10 | 11 | 12 | class Client(BaseClient, socket_type=SOCK_STREAM): 13 | """An RCON client.""" 14 | 15 | def __init__(self, *args, frag_threshold: int = 4096, **kwargs): 16 | """Set an optional fragmentation threshold and 17 | command in order to detect fragmented packets. 18 | 19 | For details see: https://wiki.vg/RCON#Fragmentation 20 | """ 21 | super().__init__(*args, **kwargs) 22 | self.frag_threshold = frag_threshold 23 | 24 | def communicate( 25 | self, packet: Packet, raise_unexpected_terminator: bool = False 26 | ) -> Packet: 27 | """Send and receive a packet.""" 28 | self.send(packet) 29 | return self.read(raise_unexpected_terminator) 30 | 31 | def send(self, packet: Packet) -> None: 32 | """Send a packet to the server.""" 33 | with self._socket.makefile("wb") as file: 34 | file.write(bytes(packet)) 35 | 36 | def read(self, raise_unexpected_terminator: bool = False) -> Packet: 37 | """Read a packet from the server.""" 38 | with self._socket.makefile("rb") as file: 39 | response = Packet.read(file, raise_unexpected_terminator) 40 | 41 | if len(response.payload) < self.frag_threshold: 42 | return response 43 | 44 | self.send(Packet.make_empty_response()) 45 | 46 | while (successor := Packet.read(file)).id == response.id: 47 | response += successor 48 | 49 | return response 50 | 51 | def login(self, passwd: str, *, encoding: str = "utf-8") -> bool: 52 | """Perform a login.""" 53 | self.send(Packet.make_login(passwd, encoding=encoding)) 54 | 55 | # Wait for SERVERDATA_AUTH_RESPONSE according to: 56 | # https://developer.valvesoftware.com/wiki/Source_RCON_Protocol 57 | while (response := self.read()).type != Type.SERVERDATA_AUTH_RESPONSE: 58 | pass 59 | 60 | if response.id == -1: 61 | raise WrongPassword() 62 | 63 | return True 64 | 65 | def run( 66 | self, 67 | command: str, 68 | *args: str, 69 | encoding: str = "utf-8", 70 | enforce_id: bool = True, 71 | raise_unexpected_terminator: bool = False, 72 | ) -> str: 73 | """Run a command.""" 74 | request = Packet.make_command(command, *args, encoding=encoding) 75 | response = self.communicate(request, raise_unexpected_terminator) 76 | 77 | if enforce_id and response.id != request.id: 78 | raise SessionTimeout("packet ID mismatch") 79 | 80 | return response.payload.decode(encoding) 81 | -------------------------------------------------------------------------------- /rcon/source/proto.py: -------------------------------------------------------------------------------- 1 | """Low-level protocol stuff.""" 2 | 3 | from __future__ import annotations 4 | from asyncio import StreamReader 5 | from enum import Enum 6 | from functools import partial 7 | from logging import getLogger 8 | from random import randint 9 | from typing import IO, NamedTuple 10 | 11 | from rcon.exceptions import EmptyResponse, UnexpectedTerminator 12 | 13 | 14 | __all__ = ["LittleEndianSignedInt32", "Type", "Packet", "random_request_id"] 15 | 16 | 17 | LOGGER = getLogger(__file__) 18 | TERMINATOR = b"\x00\x00" 19 | 20 | 21 | class LittleEndianSignedInt32(int): 22 | """A little-endian, signed int32.""" 23 | 24 | MIN = -2_147_483_648 25 | MAX = 2_147_483_647 26 | 27 | def __init__(self, *_): 28 | """Check the boundaries.""" 29 | super().__init__() 30 | 31 | if not self.MIN <= self <= self.MAX: 32 | raise ValueError("Signed int32 out of bounds:", int(self)) 33 | 34 | def __bytes__(self): 35 | """Return the integer as signed little endian.""" 36 | return self.to_bytes(4, "little", signed=True) 37 | 38 | @classmethod 39 | async def aread(cls, reader: StreamReader) -> LittleEndianSignedInt32: 40 | """Read the integer from an asynchronous file-like object.""" 41 | return cls.from_bytes(await reader.read(4), "little", signed=True) 42 | 43 | @classmethod 44 | def read(cls, file: IO) -> LittleEndianSignedInt32: 45 | """Read the integer from a file-like object.""" 46 | return cls.from_bytes(file.read(4), "little", signed=True) 47 | 48 | 49 | class Type(LittleEndianSignedInt32, Enum): 50 | """RCON packet types.""" 51 | 52 | SERVERDATA_AUTH = LittleEndianSignedInt32(3) 53 | SERVERDATA_AUTH_RESPONSE = LittleEndianSignedInt32(2) 54 | SERVERDATA_EXECCOMMAND = LittleEndianSignedInt32(2) 55 | SERVERDATA_RESPONSE_VALUE = LittleEndianSignedInt32(0) 56 | 57 | def __int__(self): 58 | """Return the actual integer value.""" 59 | return int(self.value) 60 | 61 | def __bytes__(self): 62 | """Return the integer value as little endian.""" 63 | return bytes(self.value) 64 | 65 | @classmethod 66 | async def aread(cls, reader: StreamReader, *, prefix: str = "") -> Type: 67 | """Read the type from an asynchronous file-like object.""" 68 | LOGGER.debug("%sReading type asynchronously.", prefix) 69 | value = await LittleEndianSignedInt32.aread(reader) 70 | LOGGER.debug("%s => value: %i", prefix, value) 71 | return cls(value) 72 | 73 | @classmethod 74 | def read(cls, file: IO, *, prefix: str = "") -> Type: 75 | """Read the type from a file-like object.""" 76 | LOGGER.debug("%sReading type.", prefix) 77 | value = LittleEndianSignedInt32.read(file) 78 | LOGGER.debug("%s => value: %i", prefix, value) 79 | return cls(value) 80 | 81 | 82 | class Packet(NamedTuple): 83 | """An RCON packet.""" 84 | 85 | id: LittleEndianSignedInt32 86 | type: Type 87 | payload: bytes 88 | terminator: bytes = TERMINATOR 89 | 90 | def __add__(self, other: Packet | None) -> Packet: 91 | if other is None: 92 | return self 93 | 94 | if other.id != self.id: 95 | raise ValueError("Can only add packages with same id.") 96 | 97 | if other.type != self.type: 98 | raise ValueError("Can only add packages of same type.") 99 | 100 | if other.terminator != self.terminator: 101 | raise ValueError("Can only add packages with same terminator.") 102 | 103 | return Packet(self.id, self.type, self.payload + other.payload, self.terminator) 104 | 105 | def __radd__(self, other: Packet | None) -> Packet: 106 | if other is None: 107 | return self 108 | 109 | return other.__add__(self) 110 | 111 | def __bytes__(self): 112 | """Return the packet as bytes with prepended length.""" 113 | payload = bytes(self.id) 114 | payload += bytes(self.type) 115 | payload += self.payload 116 | payload += self.terminator 117 | size = bytes(LittleEndianSignedInt32(len(payload))) 118 | return size + payload 119 | 120 | @classmethod 121 | async def aread( 122 | cls, reader: StreamReader, raise_unexpected_terminator: bool = False 123 | ) -> Packet: 124 | """Read a packet from an asynchronous file-like object.""" 125 | LOGGER.debug("Reading packet asynchronously.") 126 | size = await LittleEndianSignedInt32.aread(reader) 127 | LOGGER.debug(" => size: %i", size) 128 | 129 | if not size: 130 | raise EmptyResponse() 131 | 132 | id_ = await LittleEndianSignedInt32.aread(reader) 133 | LOGGER.debug(" => id: %i", id_) 134 | type_ = await Type.aread(reader, prefix=" ") 135 | LOGGER.debug(" => type: %i", type_) 136 | payload = await reader.read(size - 10) 137 | LOGGER.debug(" => payload: %s", payload) 138 | terminator = await reader.read(2) 139 | LOGGER.debug(" => terminator: %s", terminator) 140 | 141 | if terminator != TERMINATOR: 142 | if raise_unexpected_terminator: 143 | raise UnexpectedTerminator(terminator) 144 | LOGGER.warning("Unexpected terminator: %s", terminator) 145 | 146 | return cls(id_, type_, payload, terminator) 147 | 148 | @classmethod 149 | def read(cls, file: IO, raise_unexpected_terminator: bool = False) -> Packet: 150 | """Read a packet from a file-like object.""" 151 | LOGGER.debug("Reading packet.") 152 | size = LittleEndianSignedInt32.read(file) 153 | LOGGER.debug(" => size: %i", size) 154 | 155 | if not size: 156 | raise EmptyResponse() 157 | 158 | id_ = LittleEndianSignedInt32.read(file) 159 | LOGGER.debug(" => id: %i", id_) 160 | type_ = Type.read(file, prefix=" ") 161 | LOGGER.debug(" => type: %i", type_) 162 | payload = file.read(size - 10) 163 | LOGGER.debug(" => payload: %s", payload) 164 | terminator = file.read(2) 165 | LOGGER.debug(" => terminator: %s", terminator) 166 | 167 | if terminator != TERMINATOR: 168 | if raise_unexpected_terminator: 169 | raise UnexpectedTerminator(terminator) 170 | LOGGER.warning("Unexpected terminator: %s", terminator) 171 | 172 | return cls(id_, type_, payload, terminator) 173 | 174 | @classmethod 175 | def make_command(cls, *args: str, encoding: str = "utf-8") -> Packet: 176 | """Create a command packet.""" 177 | return cls( 178 | random_request_id(), 179 | Type.SERVERDATA_EXECCOMMAND, 180 | b" ".join(map(partial(str.encode, encoding=encoding), args)), 181 | ) 182 | 183 | @classmethod 184 | def make_empty_response(cls) -> Packet: 185 | """Create an empty response packet.""" 186 | return cls(random_request_id(), Type.SERVERDATA_RESPONSE_VALUE, b"") 187 | 188 | @classmethod 189 | def make_login(cls, passwd: str, *, encoding: str = "utf-8") -> Packet: 190 | """Create a login packet.""" 191 | return cls(random_request_id(), Type.SERVERDATA_AUTH, passwd.encode(encoding)) 192 | 193 | 194 | def random_request_id() -> LittleEndianSignedInt32: 195 | """Generate a random request ID.""" 196 | 197 | return LittleEndianSignedInt32(randint(0, LittleEndianSignedInt32.MAX)) 198 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conqp/rcon/0bf3ab5e4f612fa754579b9211dba19a8b180ac1/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """Installation script.""" 3 | 4 | from setuptools import setup 5 | 6 | setup( 7 | name="rcon", 8 | use_scm_version=True, 9 | setup_requires=["setuptools_scm"], 10 | author="Richard Neumann", 11 | author_email="mail@richard-neumann.de", 12 | python_requires=">=3.10", 13 | packages=["rcon", "rcon.battleye", "rcon.source"], 14 | extras_require={"GUI": ["pygobject", "pygtk"]}, 15 | tests_require=["pytest"], 16 | entry_points={ 17 | "console_scripts": [ 18 | "rcongui = rcon.gui:main", 19 | "rconclt = rcon.rconclt:main", 20 | "rconshell = rcon.rconshell:main", 21 | ], 22 | }, 23 | url="https://github.com/conqp/rcon", 24 | license="GPLv3", 25 | description="An RCON client library.", 26 | long_description=open("README.md").read(), 27 | long_description_content_type="text/markdown", 28 | keywords="python rcon client", 29 | ) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests.""" 2 | -------------------------------------------------------------------------------- /tests/test_battleye_proto.py: -------------------------------------------------------------------------------- 1 | """Test the BattlEye protocol.""" 2 | 3 | from unittest import TestCase 4 | 5 | from rcon.battleye.proto import Header 6 | 7 | 8 | HEADER = Header(920575337, 0x00) 9 | BYTES = b"BEi\xdd\xde6\xff\x00" 10 | 11 | 12 | class TestHeader(TestCase): 13 | """Test header object.""" 14 | 15 | def test_header_from_bytes(self): 16 | """Tests header object parsing.""" 17 | self.assertEqual(Header.from_bytes(BYTES), HEADER) 18 | 19 | def test_header_to_bytes(self): 20 | """Tests header object parsing.""" 21 | self.assertEqual(bytes(HEADER), BYTES) 22 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | """Tests the configuration file parsing.""" 2 | 3 | from itertools import product 4 | from random import shuffle 5 | from string import printable 6 | from typing import Iterator 7 | from unittest import TestCase 8 | 9 | from rcon.config import Config 10 | 11 | 12 | def random_passwd() -> str: 13 | """Generates a random password containing all printable characters.""" 14 | 15 | chars = list(printable) 16 | shuffle(chars) 17 | return "".join(chars) 18 | 19 | 20 | class TestConfig(TestCase): 21 | """Test the named tuple Config.""" 22 | 23 | def setUp(self): 24 | """Sets up test and target data.""" 25 | self.hosts = ["subsubdomain.subdomain.example.com", "locahost", "127.0.0.1"] 26 | self.ports = range(65_536) 27 | 28 | @property 29 | def _sockets(self) -> Iterator[tuple[str, int]]: 30 | """Yields (host, port) tuples.""" 31 | return product(self.hosts, self.ports) 32 | 33 | def _test_from_string_with_password(self, host, port): 34 | """Tests the Config.from_string() method with a password.""" 35 | passwd = random_passwd() 36 | config = Config.from_string(f"{passwd}@{host}:{port}") 37 | self.assertEqual(config.host, host) 38 | self.assertEqual(config.port, port) 39 | self.assertEqual(config.passwd, passwd) 40 | 41 | def _test_from_string_without_password(self, host, port): 42 | """Tests the Config.from_string() method without a password.""" 43 | config = Config.from_string(f"{host}:{port}") 44 | self.assertEqual(config.host, host) 45 | self.assertEqual(config.port, port) 46 | self.assertIsNone(config.passwd) 47 | 48 | def test_from_string(self): 49 | """Tests the Config.from_string() method.""" 50 | for host, port in self._sockets: 51 | self._test_from_string_with_password(host, port) 52 | self._test_from_string_without_password(host, port) 53 | -------------------------------------------------------------------------------- /tests/test_local_minecraft_server.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from socket import socket, AF_INET 3 | 4 | import pytest 5 | 6 | from rcon.source import Client 7 | 8 | HOST: str = "localhost" 9 | PORT: int = 25575 10 | 11 | 12 | class TestLocalMinecraftServer(TestCase): 13 | def setUp(self): 14 | self.client = Client(HOST, PORT, passwd="test") 15 | 16 | @pytest.mark.skipif( 17 | socket(AF_INET).connect_ex((HOST, PORT)) != 0, 18 | reason="requires a local Minecraft server to be running", 19 | ) 20 | def test_list_empty(self): 21 | with self.client as client: 22 | response = client.run("list") 23 | 24 | self.assertEqual(response, "There are 0 of a max of 20 players online: ") 25 | -------------------------------------------------------------------------------- /tests/test_source_proto.py: -------------------------------------------------------------------------------- 1 | """Testing of the proto module.""" 2 | 3 | from functools import partial 4 | from io import BytesIO 5 | from random import randint 6 | from unittest import TestCase 7 | 8 | from rcon.source.proto import LittleEndianSignedInt32 9 | from rcon.source.proto import Packet 10 | from rcon.source.proto import Type 11 | from rcon.source.proto import random_request_id 12 | 13 | 14 | class TestRandomRequestId(TestCase): 15 | """Tests the random_request_id function.""" 16 | 17 | def _test_type(self, request_id: LittleEndianSignedInt32): 18 | """Tests the type of a request id.""" 19 | self.assertIsInstance(request_id, LittleEndianSignedInt32) 20 | 21 | def _test_value(self, request_id: LittleEndianSignedInt32): 22 | """Tests the value of a request id.""" 23 | self.assertTrue(0 <= request_id <= LittleEndianSignedInt32.MAX) 24 | 25 | def _test_request_id(self, request_id: LittleEndianSignedInt32): 26 | """Tests a request id.""" 27 | self._test_type(request_id) 28 | self._test_value(request_id) 29 | 30 | def test_random_request_ids(self): 31 | """Tests for valid values.""" 32 | for _ in range(1000): 33 | self._test_request_id(random_request_id()) 34 | 35 | 36 | class TestLittleEndianSignedInt32(TestCase): 37 | """Tests the LittleEndianSignedInt32 type.""" 38 | 39 | def test_min(self): 40 | """Tests the minimum value.""" 41 | self.assertEqual( 42 | LittleEndianSignedInt32(LittleEndianSignedInt32.MIN), 43 | LittleEndianSignedInt32.MIN, 44 | ) 45 | 46 | def test_max(self): 47 | """Tests the maximum value.""" 48 | self.assertEqual( 49 | LittleEndianSignedInt32(LittleEndianSignedInt32.MAX), 50 | LittleEndianSignedInt32.MAX, 51 | ) 52 | 53 | def test_below_min(self): 54 | """Tests a value below the minimum value.""" 55 | self.assertRaises( 56 | ValueError, 57 | partial(LittleEndianSignedInt32, LittleEndianSignedInt32.MIN - 1), 58 | ) 59 | 60 | def test_above_max(self): 61 | """Tests a value above the maximum value.""" 62 | self.assertRaises( 63 | ValueError, 64 | partial(LittleEndianSignedInt32, LittleEndianSignedInt32.MAX + 1), 65 | ) 66 | 67 | def test_random(self): 68 | """Tests random LittleEndianSignedInt32 values.""" 69 | for _ in range(1000): 70 | random = randint( 71 | LittleEndianSignedInt32.MIN, LittleEndianSignedInt32.MAX + 1 72 | ) 73 | self.assertEqual(LittleEndianSignedInt32(random), random) 74 | 75 | 76 | class TestType(TestCase): 77 | """Tests the Type enum.""" 78 | 79 | def test_serverdata_auth_value(self): 80 | """Tests the SERVERDATA_AUTH value.""" 81 | self.assertEqual(Type.SERVERDATA_AUTH.value, 3) 82 | self.assertEqual(int(Type.SERVERDATA_AUTH), 3) 83 | 84 | def test_serverdata_auth_bytes(self): 85 | """Tests the SERVERDATA_AUTH bytes.""" 86 | self.assertEqual(bytes(Type.SERVERDATA_AUTH), (3).to_bytes(4, "little")) 87 | 88 | def test_serverdata_auth_read(self): 89 | """Tests reading of SERVERDATA_AUTH.""" 90 | self.assertIs( 91 | Type.read(BytesIO((3).to_bytes(4, "little"))), Type.SERVERDATA_AUTH 92 | ) 93 | 94 | def test_serverdata_auth_response_value(self): 95 | """Tests the SERVERDATA_AUTH_RESPONSE value.""" 96 | self.assertEqual(Type.SERVERDATA_AUTH_RESPONSE.value, 2) 97 | self.assertEqual(int(Type.SERVERDATA_AUTH_RESPONSE), 2) 98 | 99 | def test_serverdata_auth_response_bytes(self): 100 | """Tests the SERVERDATA_AUTH_RESPONSE bytes.""" 101 | self.assertEqual( 102 | bytes(Type.SERVERDATA_AUTH_RESPONSE), (2).to_bytes(4, "little") 103 | ) 104 | 105 | def test_serverdata_auth_response_read(self): 106 | """Tests the reading of SERVERDATA_AUTH_RESPONSE.""" 107 | self.assertIs( 108 | Type.read(BytesIO((2).to_bytes(4, "little"))), Type.SERVERDATA_AUTH_RESPONSE 109 | ) 110 | 111 | def test_serverdata_execcommand_value(self): 112 | """Tests the SERVERDATA_EXECCOMMAND value.""" 113 | self.assertEqual(Type.SERVERDATA_EXECCOMMAND.value, 2) 114 | self.assertEqual(int(Type.SERVERDATA_EXECCOMMAND), 2) 115 | 116 | def test_serverdata_execcommand_bytes(self): 117 | """Tests the SERVERDATA_EXECCOMMAND bytes.""" 118 | self.assertEqual(bytes(Type.SERVERDATA_EXECCOMMAND), (2).to_bytes(4, "little")) 119 | 120 | def test_serverdata_execcommand_read(self): 121 | """Tests the reading of SERVERDATA_EXECCOMMAND.""" 122 | self.assertIs( 123 | Type.read(BytesIO((2).to_bytes(4, "little"))), Type.SERVERDATA_EXECCOMMAND 124 | ) 125 | 126 | def test_serverdata_response_value_value(self): 127 | """Tests the SERVERDATA_RESPONSE_VALUE value.""" 128 | self.assertEqual(Type.SERVERDATA_RESPONSE_VALUE.value, 0) 129 | self.assertEqual(int(Type.SERVERDATA_RESPONSE_VALUE), 0) 130 | 131 | def test_serverdata_response_value_bytes(self): 132 | """Tests the SERVERDATA_RESPONSE_VALUE bytes.""" 133 | self.assertEqual( 134 | bytes(Type.SERVERDATA_RESPONSE_VALUE), (0).to_bytes(4, "little") 135 | ) 136 | 137 | def test_serverdata_response_value_read(self): 138 | """Tests the reading SERVERDATA_RESPONSE_VALUE.""" 139 | self.assertIs( 140 | Type.read(BytesIO((0).to_bytes(4, "little"))), 141 | Type.SERVERDATA_RESPONSE_VALUE, 142 | ) 143 | 144 | 145 | class TestPacket(TestCase): 146 | """Tests the Packet named tuple.""" 147 | 148 | def setUp(self): 149 | """Creates a packet.""" 150 | self.packet = Packet( 151 | random_request_id(), 152 | Type.SERVERDATA_EXECCOMMAND, 153 | "Lorem ipsum sit amet...".encode(), 154 | ) 155 | 156 | def test_bytes_rw(self): 157 | """Tests recovering from bytes.""" 158 | self.assertEqual(Packet.read(BytesIO(bytes(self.packet))), self.packet) 159 | --------------------------------------------------------------------------------