├── COPYING ├── README.rst ├── zeroconf.py ├── zunittest.py ├── zwebbrowse.py └── zwebtest.py /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Multicast DNS Service Discovery for Python 2 | ========================================== 3 | 4 | :Authors: Originally by Paul Scott-Murphy, modified by William McBrine 5 | :Version: 0.16-wmcbrine 6 | 7 | zeroconf.py is the implementation file; look at the end for examples of 8 | basic use. You can also view zwebbrowse.py to see how to browse for 9 | services, and zwebtest.py for an example of announcing them. 10 | 11 | This fork is used in all of my TiVo-related projects: HME for Python 12 | (and therefore HME/VLC), Network Remote, Remote Proxy, and pyTivo. 13 | Before this, I was tracking the changes for zeroconf.py in three 14 | separate repos. I figured I should have an authoritative source. 15 | 16 | Although I make changes based on my experience with TiVos, I expect that 17 | they're generally applicable. This version also includes patches found 18 | on the now-defunct (?) Launchpad repo of pyzeroconf, and elsewhere 19 | around the net -- not always well-documented, sorry. 20 | 21 | 22 | History 23 | ------- 24 | 25 | 0.16 26 | - Under Python 3 on Windows, there was an unsuppressed warning on 27 | closing (reported as socket.EBADF on other platforms). 28 | 29 | 0.15 30 | - Compatible with both Python 3.x and 2.x. The oldest working version 31 | is now 2.6. 32 | 33 | 0.14 34 | - Fix for SOL_IP undefined on some systems - thanks Mike Erdely. 35 | - Cleaned up examples. 36 | - Lowercased module name. 37 | 38 | 0.13 39 | - Various minor changes; see git for details. 40 | - No longer compatible with Python 2.2. Only tested with 2.5-2.7. 41 | - Fork by William McBrine. 42 | 43 | 0.12 44 | - allow selection of binding interface 45 | - typo fix - Thanks A. M. Kuchlingi 46 | - removed all use of word 'Rendezvous' - this is an API change 47 | 48 | 0.11 49 | - correction to comments for addListener method 50 | - support for new record types seen from OS X 51 | - IPv6 address 52 | - hostinfo 53 | - ignore unknown DNS record types 54 | - fixes to name decoding 55 | - works alongside other processes using port 5353 56 | - (e.g. on Mac OS X) 57 | - tested against Mac OS X 10.3.2's mDNSResponder 58 | - corrections to removal of list entries for service browser 59 | 60 | 0.10 61 | - Jonathon Paisley contributed these corrections: 62 | - always multicast replies, even when query is unicast 63 | - correct a pointer encoding problem 64 | - can now write records in any order 65 | - traceback shown on failure 66 | - better TXT record parsing 67 | - server is now separate from name 68 | - can cancel a service browser 69 | - modified some unit tests to accommodate these changes 70 | 71 | 0.09 72 | - remove all records on service unregistration 73 | - fix DOS security problem with readName 74 | 75 | 0.08 76 | - changed licensing to LGPL 77 | 78 | 0.07 79 | - faster shutdown on engine 80 | - pointer encoding of outgoing names 81 | - ServiceBrowser now works 82 | - new unit tests 83 | 84 | 0.06 85 | - small improvements with unit tests 86 | - added defined exception types 87 | - new style objects 88 | - fixed hostname/interface problem 89 | - fixed socket timeout problem 90 | - fixed addServiceListener() typo bug 91 | - using select() for socket reads 92 | - tested on Debian unstable with Python 2.2.2 93 | 94 | 0.05 95 | - ensure case insensitivty on domain names 96 | - support for unicast DNS queries 97 | 98 | 0.04 99 | - added some unit tests 100 | - added __ne__ adjuncts where required 101 | - ensure names end in '.local.' 102 | - timeout on receiving socket for clean shutdown 103 | -------------------------------------------------------------------------------- /zeroconf.py: -------------------------------------------------------------------------------- 1 | """ Multicast DNS Service Discovery for Python, v0.16-wmcbrine 2 | Copyright 2003 Paul Scott-Murphy, 2014-2020 William McBrine 3 | 4 | This module provides a framework for the use of DNS Service Discovery 5 | using IP multicast. 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General Public 18 | License along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 20 | USA 21 | 22 | """ 23 | 24 | __author__ = 'Paul Scott-Murphy' 25 | __maintainer__ = 'William McBrine ' 26 | __version__ = '0.16-wmcbrine' 27 | __license__ = 'LGPL' 28 | 29 | import sys 30 | import time 31 | import struct 32 | import socket 33 | import threading 34 | import select 35 | import traceback 36 | from functools import reduce 37 | 38 | pythree = (sys.version_info[0] >= 3) 39 | 40 | __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser", "pythree"] 41 | 42 | # hook for threads 43 | 44 | _GLOBAL_DONE = False 45 | 46 | # Some timing constants 47 | 48 | _UNREGISTER_TIME = 125 49 | _CHECK_TIME = 175 50 | _REGISTER_TIME = 225 51 | _LISTENER_TIME = 200 52 | _BROWSER_TIME = 500 53 | 54 | # Some DNS constants 55 | 56 | _MDNS_ADDR = '224.0.0.251' 57 | _MDNS_PORT = 5353 58 | _DNS_PORT = 53 59 | _DNS_TTL = 60 * 60 # one hour default TTL 60 | 61 | _MAX_MSG_TYPICAL = 1460 # unused 62 | _MAX_MSG_ABSOLUTE = 8972 63 | 64 | _FLAGS_QR_MASK = 0x8000 # query response mask 65 | _FLAGS_QR_QUERY = 0x0000 # query 66 | _FLAGS_QR_RESPONSE = 0x8000 # response 67 | 68 | _FLAGS_AA = 0x0400 # Authorative answer 69 | _FLAGS_TC = 0x0200 # Truncated 70 | _FLAGS_RD = 0x0100 # Recursion desired 71 | _FLAGS_RA = 0x8000 # Recursion available 72 | 73 | _FLAGS_Z = 0x0040 # Zero 74 | _FLAGS_AD = 0x0020 # Authentic data 75 | _FLAGS_CD = 0x0010 # Checking disabled 76 | 77 | _CLASS_IN = 1 78 | _CLASS_CS = 2 79 | _CLASS_CH = 3 80 | _CLASS_HS = 4 81 | _CLASS_NONE = 254 82 | _CLASS_ANY = 255 83 | _CLASS_MASK = 0x7FFF 84 | _CLASS_UNIQUE = 0x8000 85 | 86 | _TYPE_A = 1 87 | _TYPE_NS = 2 88 | _TYPE_MD = 3 89 | _TYPE_MF = 4 90 | _TYPE_CNAME = 5 91 | _TYPE_SOA = 6 92 | _TYPE_MB = 7 93 | _TYPE_MG = 8 94 | _TYPE_MR = 9 95 | _TYPE_NULL = 10 96 | _TYPE_WKS = 11 97 | _TYPE_PTR = 12 98 | _TYPE_HINFO = 13 99 | _TYPE_MINFO = 14 100 | _TYPE_MX = 15 101 | _TYPE_TXT = 16 102 | _TYPE_AAAA = 28 103 | _TYPE_SRV = 33 104 | _TYPE_ANY = 255 105 | 106 | # Mapping constants to names 107 | 108 | _CLASSES = { _CLASS_IN : "in", 109 | _CLASS_CS : "cs", 110 | _CLASS_CH : "ch", 111 | _CLASS_HS : "hs", 112 | _CLASS_NONE : "none", 113 | _CLASS_ANY : "any" } 114 | 115 | _TYPES = { _TYPE_A : "a", 116 | _TYPE_NS : "ns", 117 | _TYPE_MD : "md", 118 | _TYPE_MF : "mf", 119 | _TYPE_CNAME : "cname", 120 | _TYPE_SOA : "soa", 121 | _TYPE_MB : "mb", 122 | _TYPE_MG : "mg", 123 | _TYPE_MR : "mr", 124 | _TYPE_NULL : "null", 125 | _TYPE_WKS : "wks", 126 | _TYPE_PTR : "ptr", 127 | _TYPE_HINFO : "hinfo", 128 | _TYPE_MINFO : "minfo", 129 | _TYPE_MX : "mx", 130 | _TYPE_TXT : "txt", 131 | _TYPE_AAAA : "quada", 132 | _TYPE_SRV : "srv", 133 | _TYPE_ANY : "any" } 134 | 135 | # utility functions 136 | 137 | def getByte(n): 138 | if pythree: 139 | return n 140 | else: 141 | return ord(n) 142 | 143 | def putByte(n): 144 | if pythree: 145 | return n.to_bytes(1, 'little') 146 | else: 147 | return chr(n) 148 | 149 | def currentTimeMillis(): 150 | """Current system time in milliseconds""" 151 | return time.time() * 1000 152 | 153 | # Exceptions 154 | 155 | class NonLocalNameException(Exception): 156 | pass 157 | 158 | class NonUniqueNameException(Exception): 159 | pass 160 | 161 | class NamePartTooLongException(Exception): 162 | pass 163 | 164 | class AbstractMethodException(Exception): 165 | pass 166 | 167 | class BadTypeInNameException(Exception): 168 | pass 169 | 170 | # implementation classes 171 | 172 | class DNSEntry(object): 173 | """A DNS entry""" 174 | 175 | def __init__(self, name, type, clazz): 176 | self.key = name.lower() 177 | self.name = name 178 | self.type = type 179 | self.clazz = clazz & _CLASS_MASK 180 | self.unique = (clazz & _CLASS_UNIQUE) != 0 181 | 182 | def __eq__(self, other): 183 | """Equality test on name, type, and class""" 184 | return (isinstance(other, DNSEntry) and 185 | self.name == other.name and 186 | self.type == other.type and 187 | self.clazz == other.clazz) 188 | 189 | def __ne__(self, other): 190 | """Non-equality test""" 191 | return not self.__eq__(other) 192 | 193 | def getClazz(self, clazz): 194 | """Class accessor""" 195 | return _CLASSES.get(clazz, "?(%s)" % clazz) 196 | 197 | def getType(self, t): 198 | """Type accessor""" 199 | return _TYPES.get(t, "?(%s)" % t) 200 | 201 | def toString(self, hdr, other): 202 | """String representation with additional information""" 203 | result = "%s[%s,%s" % (hdr, self.getType(self.type), 204 | self.getClazz(self.clazz)) 205 | if self.unique: 206 | result += "-unique," 207 | else: 208 | result += "," 209 | result += self.name 210 | if other is not None: 211 | result += ",%s]" % (other) 212 | else: 213 | result += "]" 214 | return result 215 | 216 | class DNSQuestion(DNSEntry): 217 | """A DNS question entry""" 218 | 219 | def __init__(self, name, type, clazz): 220 | #if not name.endswith(".local."): 221 | # raise NonLocalNameException 222 | DNSEntry.__init__(self, name, type, clazz) 223 | 224 | def answeredBy(self, rec): 225 | """Returns true if the question is answered by the record""" 226 | return (self.clazz == rec.clazz and 227 | (self.type == rec.type or self.type == _TYPE_ANY) and 228 | self.name == rec.name) 229 | 230 | def __repr__(self): 231 | """String representation""" 232 | return DNSEntry.toString(self, "question", None) 233 | 234 | 235 | class DNSRecord(DNSEntry): 236 | """A DNS record - like a DNS entry, but has a TTL""" 237 | 238 | def __init__(self, name, type, clazz, ttl): 239 | DNSEntry.__init__(self, name, type, clazz) 240 | self.ttl = ttl 241 | self.created = currentTimeMillis() 242 | 243 | def __eq__(self, other): 244 | """Tests equality as per DNSRecord""" 245 | return isinstance(other, DNSRecord) and DNSEntry.__eq__(self, other) 246 | 247 | def suppressedBy(self, msg): 248 | """Returns true if any answer in a message can suffice for the 249 | information held in this record.""" 250 | for record in msg.answers: 251 | if self.suppressedByAnswer(record): 252 | return True 253 | return False 254 | 255 | def suppressedByAnswer(self, other): 256 | """Returns true if another record has same name, type and class, 257 | and if its TTL is at least half of this record's.""" 258 | return self == other and other.ttl > (self.ttl / 2) 259 | 260 | def getExpirationTime(self, percent): 261 | """Returns the time at which this record will have expired 262 | by a certain percentage.""" 263 | return self.created + (percent * self.ttl * 10) 264 | 265 | def getRemainingTTL(self, now): 266 | """Returns the remaining TTL in seconds.""" 267 | return max(0, (self.getExpirationTime(100) - now) / 1000) 268 | 269 | def isExpired(self, now): 270 | """Returns true if this record has expired.""" 271 | return self.getExpirationTime(100) <= now 272 | 273 | def isStale(self, now): 274 | """Returns true if this record is at least half way expired.""" 275 | return self.getExpirationTime(50) <= now 276 | 277 | def resetTTL(self, other): 278 | """Sets this record's TTL and created time to that of 279 | another record.""" 280 | self.created = other.created 281 | self.ttl = other.ttl 282 | 283 | def write(self, out): 284 | """Abstract method""" 285 | raise AbstractMethodException 286 | 287 | def toString(self, other): 288 | """String representation with addtional information""" 289 | arg = "%s/%s,%s" % (self.ttl, 290 | self.getRemainingTTL(currentTimeMillis()), other) 291 | return DNSEntry.toString(self, "record", arg) 292 | 293 | class DNSAddress(DNSRecord): 294 | """A DNS address record""" 295 | 296 | def __init__(self, name, type, clazz, ttl, address): 297 | DNSRecord.__init__(self, name, type, clazz, ttl) 298 | self.address = address 299 | 300 | def write(self, out): 301 | """Used in constructing an outgoing packet""" 302 | out.writeString(self.address) 303 | 304 | def __eq__(self, other): 305 | """Tests equality on address""" 306 | return isinstance(other, DNSAddress) and self.address == other.address 307 | 308 | def __repr__(self): 309 | """String representation""" 310 | try: 311 | return socket.inet_ntoa(self.address) 312 | except: 313 | return self.address 314 | 315 | class DNSHinfo(DNSRecord): 316 | """A DNS host information record""" 317 | 318 | def __init__(self, name, type, clazz, ttl, cpu, os): 319 | DNSRecord.__init__(self, name, type, clazz, ttl) 320 | self.cpu = cpu 321 | self.os = os 322 | 323 | def write(self, out): 324 | """Used in constructing an outgoing packet""" 325 | out.writeString(self.cpu) 326 | out.writeString(self.oso) 327 | 328 | def __eq__(self, other): 329 | """Tests equality on cpu and os""" 330 | return (isinstance(other, DNSHinfo) and 331 | self.cpu == other.cpu and self.os == other.os) 332 | 333 | def __repr__(self): 334 | """String representation""" 335 | return self.cpu + " " + self.os 336 | 337 | class DNSPointer(DNSRecord): 338 | """A DNS pointer record""" 339 | 340 | def __init__(self, name, type, clazz, ttl, alias): 341 | DNSRecord.__init__(self, name, type, clazz, ttl) 342 | self.alias = alias 343 | 344 | def write(self, out): 345 | """Used in constructing an outgoing packet""" 346 | out.writeName(self.alias) 347 | 348 | def __eq__(self, other): 349 | """Tests equality on alias""" 350 | return isinstance(other, DNSPointer) and self.alias == other.alias 351 | 352 | def __repr__(self): 353 | """String representation""" 354 | return self.toString(self.alias) 355 | 356 | class DNSText(DNSRecord): 357 | """A DNS text record""" 358 | 359 | def __init__(self, name, type, clazz, ttl, text): 360 | DNSRecord.__init__(self, name, type, clazz, ttl) 361 | self.text = text 362 | 363 | def write(self, out): 364 | """Used in constructing an outgoing packet""" 365 | out.writeString(self.text) 366 | 367 | def __eq__(self, other): 368 | """Tests equality on text""" 369 | return isinstance(other, DNSText) and self.text == other.text 370 | 371 | def __repr__(self): 372 | """String representation""" 373 | if len(self.text) > 10: 374 | return self.toString(self.text[:7] + "...") 375 | else: 376 | return self.toString(self.text) 377 | 378 | class DNSService(DNSRecord): 379 | """A DNS service record""" 380 | 381 | def __init__(self, name, type, clazz, ttl, priority, weight, port, server): 382 | DNSRecord.__init__(self, name, type, clazz, ttl) 383 | self.priority = priority 384 | self.weight = weight 385 | self.port = port 386 | self.server = server 387 | 388 | def write(self, out): 389 | """Used in constructing an outgoing packet""" 390 | out.writeShort(self.priority) 391 | out.writeShort(self.weight) 392 | out.writeShort(self.port) 393 | out.writeName(self.server) 394 | 395 | def __eq__(self, other): 396 | """Tests equality on priority, weight, port and server""" 397 | return (isinstance(other, DNSService) and 398 | self.priority == other.priority and 399 | self.weight == other.weight and 400 | self.port == other.port and 401 | self.server == other.server) 402 | 403 | def __repr__(self): 404 | """String representation""" 405 | return self.toString("%s:%s" % (self.server, self.port)) 406 | 407 | class DNSIncoming(object): 408 | """Object representation of an incoming DNS packet""" 409 | 410 | def __init__(self, data): 411 | """Constructor from string holding bytes of packet""" 412 | self.offset = 0 413 | self.data = data 414 | self.questions = [] 415 | self.answers = [] 416 | self.numQuestions = 0 417 | self.numAnswers = 0 418 | self.numAuthorities = 0 419 | self.numAdditionals = 0 420 | 421 | self.readHeader() 422 | self.readQuestions() 423 | self.readOthers() 424 | 425 | def unpack(self, format): 426 | length = struct.calcsize(format) 427 | info = struct.unpack(format, self.data[self.offset:self.offset+length]) 428 | self.offset += length 429 | return info 430 | 431 | def readHeader(self): 432 | """Reads header portion of packet""" 433 | (self.id, self.flags, self.numQuestions, self.numAnswers, 434 | self.numAuthorities, self.numAdditionals) = self.unpack('!6H') 435 | 436 | def readQuestions(self): 437 | """Reads questions section of packet""" 438 | for i in range(self.numQuestions): 439 | name = self.readName() 440 | type, clazz = self.unpack('!HH') 441 | 442 | question = DNSQuestion(name, type, clazz) 443 | self.questions.append(question) 444 | 445 | def readInt(self): 446 | """Reads an integer from the packet""" 447 | return self.unpack('!I')[0] 448 | 449 | def readCharacterString(self): 450 | """Reads a character string from the packet""" 451 | length = getByte(self.data[self.offset]) 452 | self.offset += 1 453 | s = self.readString(length) 454 | if pythree: 455 | s = s.decode('utf-8') 456 | return s 457 | 458 | def readString(self, length): 459 | """Reads a string of a given length from the packet""" 460 | info = self.data[self.offset:self.offset+length] 461 | self.offset += length 462 | return info 463 | 464 | def readUnsignedShort(self): 465 | """Reads an unsigned short from the packet""" 466 | return self.unpack('!H')[0] 467 | 468 | def readOthers(self): 469 | """Reads the answers, authorities and additionals section of the 470 | packet""" 471 | n = self.numAnswers + self.numAuthorities + self.numAdditionals 472 | for i in range(n): 473 | domain = self.readName() 474 | type, clazz, ttl, length = self.unpack('!HHiH') 475 | 476 | rec = None 477 | if type == _TYPE_A: 478 | rec = DNSAddress(domain, type, clazz, ttl, self.readString(4)) 479 | elif type == _TYPE_CNAME or type == _TYPE_PTR: 480 | rec = DNSPointer(domain, type, clazz, ttl, self.readName()) 481 | elif type == _TYPE_TXT: 482 | rec = DNSText(domain, type, clazz, ttl, self.readString(length)) 483 | elif type == _TYPE_SRV: 484 | rec = DNSService(domain, type, clazz, ttl, 485 | self.readUnsignedShort(), self.readUnsignedShort(), 486 | self.readUnsignedShort(), self.readName()) 487 | elif type == _TYPE_HINFO: 488 | rec = DNSHinfo(domain, type, clazz, ttl, 489 | self.readCharacterString(), self.readCharacterString()) 490 | elif type == _TYPE_AAAA: 491 | rec = DNSAddress(domain, type, clazz, ttl, self.readString(16)) 492 | else: 493 | # Try to ignore types we don't know about 494 | # Skip the payload for the resource record so the next 495 | # records can be parsed correctly 496 | self.offset += length 497 | 498 | if rec is not None: 499 | self.answers.append(rec) 500 | 501 | def isQuery(self): 502 | """Returns true if this is a query""" 503 | return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY 504 | 505 | def isResponse(self): 506 | """Returns true if this is a response""" 507 | return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE 508 | 509 | def readUTF(self, offset, length): 510 | """Reads a UTF-8 string of a given length from the packet""" 511 | return self.data[offset:offset+length].decode('utf-8', 'replace') 512 | 513 | def readName(self): 514 | """Reads a domain name from the packet""" 515 | result = '' 516 | off = self.offset 517 | next = -1 518 | first = off 519 | 520 | while True: 521 | length = getByte(self.data[off]) 522 | off += 1 523 | if length == 0: 524 | break 525 | t = length & 0xC0 526 | if t == 0x00: 527 | result = ''.join((result, self.readUTF(off, length) + '.')) 528 | off += length 529 | elif t == 0xC0: 530 | if next < 0: 531 | next = off + 1 532 | off = ((length & 0x3F) << 8) | getByte(self.data[off]) 533 | if off >= first: 534 | raise Exception("Bad domain name (circular) at " + str(off)) 535 | first = off 536 | else: 537 | raise Exception("Bad domain name at " + str(off)) 538 | 539 | if next >= 0: 540 | self.offset = next 541 | else: 542 | self.offset = off 543 | 544 | return result 545 | 546 | 547 | class DNSOutgoing(object): 548 | """Object representation of an outgoing packet""" 549 | 550 | def __init__(self, flags, multicast=True): 551 | self.finished = False 552 | self.id = 0 553 | self.multicast = multicast 554 | self.flags = flags 555 | self.names = {} 556 | self.data = b'' 557 | self.size = 12 558 | 559 | self.questions = [] 560 | self.answers = [] 561 | self.authorities = [] 562 | self.additionals = [] 563 | 564 | def addQuestion(self, record): 565 | """Adds a question""" 566 | self.questions.append(record) 567 | 568 | def addAnswer(self, inp, record): 569 | """Adds an answer""" 570 | if not record.suppressedBy(inp): 571 | self.addAnswerAtTime(record, 0) 572 | 573 | def addAnswerAtTime(self, record, now): 574 | """Adds an answer if if does not expire by a certain time""" 575 | if record is not None: 576 | if now == 0 or not record.isExpired(now): 577 | self.answers.append((record, now)) 578 | 579 | def addAuthorativeAnswer(self, record): 580 | """Adds an authoritative answer""" 581 | self.authorities.append(record) 582 | 583 | def addAdditionalAnswer(self, record): 584 | """Adds an additional answer""" 585 | self.additionals.append(record) 586 | 587 | def pack(self, format, value): 588 | self.data += struct.pack(format, value) 589 | self.size += struct.calcsize(format) 590 | 591 | def writeByte(self, value): 592 | """Writes a single byte to the packet""" 593 | self.pack('!c', putByte(value)) 594 | 595 | def insertShort(self, index, value): 596 | """Inserts an unsigned short in a certain position in the packet""" 597 | self.data = self.data[:index] + struct.pack('!H', value) + \ 598 | self.data[index:] 599 | self.size += 2 600 | 601 | def writeShort(self, value): 602 | """Writes an unsigned short to the packet""" 603 | self.pack('!H', value) 604 | 605 | def writeInt(self, value): 606 | """Writes an unsigned integer to the packet""" 607 | self.pack('!I', int(value)) 608 | 609 | def writeString(self, value): 610 | """Writes a string to the packet""" 611 | if bytes != type(value): 612 | value = value.encode('utf-8') 613 | self.data += value 614 | self.size += len(value) 615 | 616 | def writeUTF(self, s): 617 | """Writes a UTF-8 string of a given length to the packet""" 618 | utfstr = s.encode('utf-8') 619 | length = len(utfstr) 620 | if length > 64: 621 | raise NamePartTooLongException 622 | self.writeByte(length) 623 | self.writeString(utfstr) 624 | 625 | def writeName(self, name): 626 | """Writes a domain name to the packet""" 627 | 628 | if name in self.names: 629 | # Find existing instance of this name in packet 630 | # 631 | index = self.names[name] 632 | 633 | # An index was found, so write a pointer to it 634 | # 635 | self.writeByte((index >> 8) | 0xC0) 636 | self.writeByte(index & 0xFF) 637 | else: 638 | # No record of this name already, so write it 639 | # out as normal, recording the location of the name 640 | # for future pointers to it. 641 | # 642 | self.names[name] = self.size 643 | parts = name.split('.') 644 | if parts[-1] == '': 645 | parts = parts[:-1] 646 | for part in parts: 647 | self.writeUTF(part) 648 | self.writeByte(0) 649 | 650 | def writeQuestion(self, question): 651 | """Writes a question to the packet""" 652 | self.writeName(question.name) 653 | self.writeShort(question.type) 654 | self.writeShort(question.clazz) 655 | 656 | def writeRecord(self, record, now): 657 | """Writes a record (answer, authoritative answer, additional) to 658 | the packet""" 659 | self.writeName(record.name) 660 | self.writeShort(record.type) 661 | if record.unique and self.multicast: 662 | self.writeShort(record.clazz | _CLASS_UNIQUE) 663 | else: 664 | self.writeShort(record.clazz) 665 | if now == 0: 666 | self.writeInt(record.ttl) 667 | else: 668 | self.writeInt(record.getRemainingTTL(now)) 669 | index = len(self.data) 670 | # Adjust size for the short we will write before this record 671 | # 672 | self.size += 2 673 | record.write(self) 674 | self.size -= 2 675 | 676 | length = len(self.data[index:]) 677 | self.insertShort(index, length) # Here is the short we adjusted for 678 | 679 | def packet(self): 680 | """Returns a string containing the packet's bytes 681 | 682 | No further parts should be added to the packet once this 683 | is done.""" 684 | if not self.finished: 685 | self.finished = True 686 | for question in self.questions: 687 | self.writeQuestion(question) 688 | for answer, time in self.answers: 689 | self.writeRecord(answer, time) 690 | for authority in self.authorities: 691 | self.writeRecord(authority, 0) 692 | for additional in self.additionals: 693 | self.writeRecord(additional, 0) 694 | 695 | self.insertShort(0, len(self.additionals)) 696 | self.insertShort(0, len(self.authorities)) 697 | self.insertShort(0, len(self.answers)) 698 | self.insertShort(0, len(self.questions)) 699 | self.insertShort(0, self.flags) 700 | if self.multicast: 701 | self.insertShort(0, 0) 702 | else: 703 | self.insertShort(0, self.id) 704 | return self.data 705 | 706 | 707 | class DNSCache(object): 708 | """A cache of DNS entries""" 709 | 710 | def __init__(self): 711 | self.cache = {} 712 | 713 | def add(self, entry): 714 | """Adds an entry""" 715 | try: 716 | list = self.cache[entry.key] 717 | except: 718 | list = self.cache[entry.key] = [] 719 | list.append(entry) 720 | 721 | def remove(self, entry): 722 | """Removes an entry""" 723 | try: 724 | list = self.cache[entry.key] 725 | list.remove(entry) 726 | except: 727 | pass 728 | 729 | def get(self, entry): 730 | """Gets an entry by key. Will return None if there is no 731 | matching entry.""" 732 | try: 733 | list = self.cache[entry.key] 734 | return list[list.index(entry)] 735 | except: 736 | return None 737 | 738 | def getByDetails(self, name, type, clazz): 739 | """Gets an entry by details. Will return None if there is 740 | no matching entry.""" 741 | entry = DNSEntry(name, type, clazz) 742 | return self.get(entry) 743 | 744 | def entriesWithName(self, name): 745 | """Returns a list of entries whose key matches the name.""" 746 | try: 747 | return self.cache[name] 748 | except: 749 | return [] 750 | 751 | def entries(self): 752 | """Returns a list of all entries""" 753 | def add(x, y): return x + y 754 | try: 755 | return reduce(add, self.cache.values()) 756 | except: 757 | return [] 758 | 759 | 760 | class Engine(threading.Thread): 761 | """An engine wraps read access to sockets, allowing objects that 762 | need to receive data from sockets to be called back when the 763 | sockets are ready. 764 | 765 | A reader needs a handle_read() method, which is called when the socket 766 | it is interested in is ready for reading. 767 | 768 | Writers are not implemented here, because we only send short 769 | packets. 770 | """ 771 | 772 | def __init__(self, zc): 773 | threading.Thread.__init__(self) 774 | self.zc = zc 775 | self.readers = {} # maps socket to reader 776 | self.timeout = 5 777 | self.condition = threading.Condition() 778 | self.start() 779 | 780 | def run(self): 781 | while not _GLOBAL_DONE: 782 | rs = self.getReaders() 783 | if len(rs) == 0: 784 | # No sockets to manage, but we wait for the timeout 785 | # or addition of a socket 786 | # 787 | self.condition.acquire() 788 | self.condition.wait(self.timeout) 789 | self.condition.release() 790 | else: 791 | try: 792 | rr, wr, er = select.select(rs, [], [], self.timeout) 793 | for socket in rr: 794 | try: 795 | self.readers[socket].handle_read() 796 | except: 797 | traceback.print_exc() 798 | except: 799 | pass 800 | 801 | def getReaders(self): 802 | self.condition.acquire() 803 | result = list(self.readers.keys()) 804 | self.condition.release() 805 | return result 806 | 807 | def addReader(self, reader, socket): 808 | self.condition.acquire() 809 | self.readers[socket] = reader 810 | self.condition.notify() 811 | self.condition.release() 812 | 813 | def delReader(self, socket): 814 | self.condition.acquire() 815 | del(self.readers[socket]) 816 | self.condition.notify() 817 | self.condition.release() 818 | 819 | def notify(self): 820 | self.condition.acquire() 821 | self.condition.notify() 822 | self.condition.release() 823 | 824 | class Listener(object): 825 | """A Listener is used by this module to listen on the multicast 826 | group to which DNS messages are sent, allowing the implementation 827 | to cache information as it arrives. 828 | 829 | It requires registration with an Engine object in order to have 830 | the read() method called when a socket is availble for reading.""" 831 | 832 | def __init__(self, zc): 833 | self.zc = zc 834 | self.zc.engine.addReader(self, self.zc.socket) 835 | 836 | def handle_read(self): 837 | try: 838 | data, (addr, port) = self.zc.socket.recvfrom(_MAX_MSG_ABSOLUTE) 839 | except socket.error as e: 840 | # If the socket was closed by another thread -- which happens 841 | # regularly on shutdown -- an EBADF exception is thrown here. 842 | # (Under Windows, it instead appears as error 10038.) Ignore it. 843 | if e.args[0] in (socket.EBADF, 10038): 844 | return 845 | else: 846 | raise 847 | self.data = data 848 | msg = DNSIncoming(data) 849 | if msg.isQuery(): 850 | # Always multicast responses 851 | # 852 | if port == _MDNS_PORT: 853 | self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) 854 | # If it's not a multicast query, reply via unicast 855 | # and multicast 856 | # 857 | elif port == _DNS_PORT: 858 | self.zc.handleQuery(msg, addr, port) 859 | self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT) 860 | else: 861 | self.zc.handleResponse(msg) 862 | 863 | 864 | class Reaper(threading.Thread): 865 | """A Reaper is used by this module to remove cache entries that 866 | have expired.""" 867 | 868 | def __init__(self, zc): 869 | threading.Thread.__init__(self) 870 | self.zc = zc 871 | self.start() 872 | 873 | def run(self): 874 | while True: 875 | self.zc.wait(10 * 1000) 876 | if _GLOBAL_DONE: 877 | return 878 | now = currentTimeMillis() 879 | for record in self.zc.cache.entries(): 880 | if record.isExpired(now): 881 | self.zc.updateRecord(now, record) 882 | self.zc.cache.remove(record) 883 | 884 | 885 | class ServiceBrowser(threading.Thread): 886 | """Used to browse for a service of a specific type. 887 | 888 | The listener object will have its addService() and 889 | removeService() methods called when this browser 890 | discovers changes in the services availability.""" 891 | 892 | def __init__(self, zc, type, listener): 893 | """Creates a browser for a specific type""" 894 | threading.Thread.__init__(self) 895 | self.zc = zc 896 | self.type = type 897 | self.listener = listener 898 | self.services = {} 899 | self.nextTime = currentTimeMillis() 900 | self.delay = _BROWSER_TIME 901 | self.list = [] 902 | 903 | self.done = False 904 | 905 | self.zc.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) 906 | self.start() 907 | 908 | def updateRecord(self, zc, now, record): 909 | """Callback invoked by Zeroconf when new information arrives. 910 | 911 | Updates information required by browser in the Zeroconf cache.""" 912 | if record.type == _TYPE_PTR and record.name == self.type: 913 | expired = record.isExpired(now) 914 | try: 915 | oldrecord = self.services[record.alias.lower()] 916 | if not expired: 917 | oldrecord.resetTTL(record) 918 | else: 919 | del(self.services[record.alias.lower()]) 920 | callback = lambda x: self.listener.removeService(x, 921 | self.type, record.alias) 922 | self.list.append(callback) 923 | return 924 | except: 925 | if not expired: 926 | self.services[record.alias.lower()] = record 927 | callback = lambda x: self.listener.addService(x, 928 | self.type, record.alias) 929 | self.list.append(callback) 930 | 931 | expires = record.getExpirationTime(75) 932 | if expires < self.nextTime: 933 | self.nextTime = expires 934 | 935 | def cancel(self): 936 | self.done = True 937 | self.zc.notifyAll() 938 | 939 | def run(self): 940 | while True: 941 | event = None 942 | now = currentTimeMillis() 943 | if len(self.list) == 0 and self.nextTime > now: 944 | self.zc.wait(self.nextTime - now) 945 | if _GLOBAL_DONE or self.done: 946 | return 947 | now = currentTimeMillis() 948 | 949 | if self.nextTime <= now: 950 | out = DNSOutgoing(_FLAGS_QR_QUERY) 951 | out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)) 952 | for record in self.services.values(): 953 | if not record.isExpired(now): 954 | out.addAnswerAtTime(record, now) 955 | self.zc.send(out) 956 | self.nextTime = now + self.delay 957 | self.delay = min(20 * 1000, self.delay * 2) 958 | 959 | if len(self.list) > 0: 960 | event = self.list.pop(0) 961 | 962 | if event is not None: 963 | event(self.zc) 964 | 965 | 966 | class ServiceInfo(object): 967 | """Service information""" 968 | 969 | def __init__(self, type, name, address=None, port=None, weight=0, 970 | priority=0, properties=None, server=None): 971 | """Create a service description. 972 | 973 | type: fully qualified service type name 974 | name: fully qualified service name 975 | address: IP address as unsigned short, network byte order 976 | port: port that the service runs on 977 | weight: weight of the service 978 | priority: priority of the service 979 | properties: dictionary of properties (or a string holding the 980 | bytes for the text field) 981 | server: fully qualified name for service host (defaults to name)""" 982 | 983 | if not name.endswith(type): 984 | raise BadTypeInNameException 985 | self.type = type 986 | self.name = name 987 | self.address = address 988 | self.port = port 989 | self.weight = weight 990 | self.priority = priority 991 | if server: 992 | self.server = server 993 | else: 994 | self.server = name 995 | self.setProperties(properties) 996 | 997 | def setProperties(self, properties): 998 | """Sets properties and text of this info from a dictionary""" 999 | if isinstance(properties, dict): 1000 | self.properties = properties 1001 | list = [] 1002 | result = b'' 1003 | for key in properties: 1004 | value = properties[key] 1005 | if value is None: 1006 | suffix = '' 1007 | elif isinstance(value, str): 1008 | suffix = value 1009 | elif isinstance(value, int): 1010 | if value: 1011 | suffix = 'true' 1012 | else: 1013 | suffix = 'false' 1014 | else: 1015 | suffix = '' 1016 | list.append('='.join((key, suffix))) 1017 | for item in list: 1018 | if bytes != type(item): 1019 | item = item.encode('utf-8') 1020 | result += putByte(len(item)) 1021 | result += item 1022 | self.text = result 1023 | else: 1024 | self.text = properties 1025 | 1026 | def setText(self, text): 1027 | """Sets properties and text given a text field""" 1028 | self.text = text 1029 | try: 1030 | result = {} 1031 | end = len(text) 1032 | index = 0 1033 | strs = [] 1034 | while index < end: 1035 | length = getByte(text[index]) 1036 | index += 1 1037 | val = text[index:index+length] 1038 | if pythree: 1039 | val = val.decode('utf-8') 1040 | strs.append(val) 1041 | index += length 1042 | 1043 | for s in strs: 1044 | try: 1045 | key, value = s.split('=', 1) 1046 | if value == 'true': 1047 | value = True 1048 | elif value == 'false' or not value: 1049 | value = False 1050 | except: 1051 | # No equals sign at all 1052 | key = s 1053 | value = False 1054 | 1055 | # Only update non-existent properties 1056 | if key and result.get(key) == None: 1057 | result[key] = value 1058 | 1059 | self.properties = result 1060 | except: 1061 | traceback.print_exc() 1062 | self.properties = None 1063 | 1064 | def getType(self): 1065 | """Type accessor""" 1066 | return self.type 1067 | 1068 | def getName(self): 1069 | """Name accessor""" 1070 | if self.type is not None and self.name.endswith("." + self.type): 1071 | return self.name[:len(self.name) - len(self.type) - 1] 1072 | return self.name 1073 | 1074 | def getAddress(self): 1075 | """Address accessor""" 1076 | return self.address 1077 | 1078 | def getPort(self): 1079 | """Port accessor""" 1080 | return self.port 1081 | 1082 | def getPriority(self): 1083 | """Pirority accessor""" 1084 | return self.priority 1085 | 1086 | def getWeight(self): 1087 | """Weight accessor""" 1088 | return self.weight 1089 | 1090 | def getProperties(self): 1091 | """Properties accessor""" 1092 | return self.properties 1093 | 1094 | def getText(self): 1095 | """Text accessor""" 1096 | return self.text 1097 | 1098 | def getServer(self): 1099 | """Server accessor""" 1100 | return self.server 1101 | 1102 | def updateRecord(self, zc, now, record): 1103 | """Updates service information from a DNS record""" 1104 | if record is not None and not record.isExpired(now): 1105 | if record.type == _TYPE_A: 1106 | #if record.name == self.name: 1107 | if record.name == self.server: 1108 | self.address = record.address 1109 | elif record.type == _TYPE_SRV: 1110 | if record.name == self.name: 1111 | self.server = record.server 1112 | self.port = record.port 1113 | self.weight = record.weight 1114 | self.priority = record.priority 1115 | #self.address = None 1116 | self.updateRecord(zc, now, 1117 | zc.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN)) 1118 | elif record.type == _TYPE_TXT: 1119 | if record.name == self.name: 1120 | self.setText(record.text) 1121 | 1122 | def request(self, zc, timeout): 1123 | """Returns true if the service could be discovered on the 1124 | network, and updates this object with details discovered. 1125 | """ 1126 | now = currentTimeMillis() 1127 | delay = _LISTENER_TIME 1128 | next = now + delay 1129 | last = now + timeout 1130 | result = False 1131 | try: 1132 | zc.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)) 1133 | while (self.server is None or self.address is None or 1134 | self.text is None): 1135 | if last <= now: 1136 | return False 1137 | if next <= now: 1138 | out = DNSOutgoing(_FLAGS_QR_QUERY) 1139 | out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, 1140 | _CLASS_IN)) 1141 | out.addAnswerAtTime(zc.cache.getByDetails(self.name, 1142 | _TYPE_SRV, _CLASS_IN), now) 1143 | out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, 1144 | _CLASS_IN)) 1145 | out.addAnswerAtTime(zc.cache.getByDetails(self.name, 1146 | _TYPE_TXT, _CLASS_IN), now) 1147 | if self.server is not None: 1148 | out.addQuestion(DNSQuestion(self.server, 1149 | _TYPE_A, _CLASS_IN)) 1150 | out.addAnswerAtTime(zc.cache.getByDetails(self.server, 1151 | _TYPE_A, _CLASS_IN), now) 1152 | zc.send(out) 1153 | next = now + delay 1154 | delay = delay * 2 1155 | 1156 | zc.wait(min(next, last) - now) 1157 | now = currentTimeMillis() 1158 | result = True 1159 | finally: 1160 | zc.removeListener(self) 1161 | 1162 | return result 1163 | 1164 | def __eq__(self, other): 1165 | """Tests equality of service name""" 1166 | if isinstance(other, ServiceInfo): 1167 | return other.name == self.name 1168 | return False 1169 | 1170 | def __ne__(self, other): 1171 | """Non-equality test""" 1172 | return not self.__eq__(other) 1173 | 1174 | def __repr__(self): 1175 | """String representation""" 1176 | result = "service[%s,%s:%s," % (self.name, 1177 | socket.inet_ntoa(self.getAddress()), self.port) 1178 | if self.text is None: 1179 | result += "None" 1180 | else: 1181 | if len(self.text) < 20: 1182 | result += str(self.text) 1183 | else: 1184 | result += str(self.text[:17]) + "..." 1185 | result += "]" 1186 | return result 1187 | 1188 | 1189 | class Zeroconf(object): 1190 | """Implementation of Zeroconf Multicast DNS Service Discovery 1191 | 1192 | Supports registration, unregistration, queries and browsing. 1193 | """ 1194 | def __init__(self, bindaddress=None): 1195 | """Creates an instance of the Zeroconf class, establishing 1196 | multicast communications, listening and reaping threads.""" 1197 | global _GLOBAL_DONE 1198 | _GLOBAL_DONE = False 1199 | if bindaddress is None: 1200 | try: 1201 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 1202 | s.connect(('4.2.2.1', 123)) 1203 | self.intf = s.getsockname()[0] 1204 | s.close() 1205 | except: 1206 | self.intf = socket.gethostbyname(socket.gethostname()) 1207 | else: 1208 | self.intf = bindaddress 1209 | self.group = ('', _MDNS_PORT) 1210 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 1211 | try: 1212 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 1213 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 1214 | except: 1215 | # SO_REUSEADDR should be equivalent to SO_REUSEPORT for 1216 | # multicast UDP sockets (p 731, "TCP/IP Illustrated, 1217 | # Volume 2"), but some BSD-derived systems require 1218 | # SO_REUSEPORT to be specified explicity. Also, not all 1219 | # versions of Python have SO_REUSEPORT available. 1220 | # 1221 | pass 1222 | self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) 1223 | self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) 1224 | try: 1225 | self.socket.bind(self.group) 1226 | except: 1227 | # Some versions of linux raise an exception even though 1228 | # the SO_REUSE* options have been set, so ignore it 1229 | # 1230 | pass 1231 | #self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, 1232 | # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0')) 1233 | self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, 1234 | socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0')) 1235 | 1236 | self.listeners = [] 1237 | self.browsers = [] 1238 | self.services = {} 1239 | self.servicetypes = {} 1240 | 1241 | self.cache = DNSCache() 1242 | 1243 | self.condition = threading.Condition() 1244 | 1245 | self.engine = Engine(self) 1246 | self.listener = Listener(self) 1247 | self.reaper = Reaper(self) 1248 | 1249 | def isLoopback(self): 1250 | return self.intf.startswith("127.0.0.1") 1251 | 1252 | def isLinklocal(self): 1253 | return self.intf.startswith("169.254.") 1254 | 1255 | def wait(self, timeout): 1256 | """Calling thread waits for a given number of milliseconds or 1257 | until notified.""" 1258 | self.condition.acquire() 1259 | self.condition.wait(timeout / 1000) 1260 | self.condition.release() 1261 | 1262 | def notifyAll(self): 1263 | """Notifies all waiting threads""" 1264 | self.condition.acquire() 1265 | self.condition.notifyAll() 1266 | self.condition.release() 1267 | 1268 | def getServiceInfo(self, type, name, timeout=3000): 1269 | """Returns network's service information for a particular 1270 | name and type, or None if no service matches by the timeout, 1271 | which defaults to 3 seconds.""" 1272 | info = ServiceInfo(type, name) 1273 | if info.request(self, timeout): 1274 | return info 1275 | return None 1276 | 1277 | def addServiceListener(self, type, listener): 1278 | """Adds a listener for a particular service type. This object 1279 | will then have its updateRecord method called when information 1280 | arrives for that type.""" 1281 | self.removeServiceListener(listener) 1282 | self.browsers.append(ServiceBrowser(self, type, listener)) 1283 | 1284 | def removeServiceListener(self, listener): 1285 | """Removes a listener from the set that is currently listening.""" 1286 | for browser in self.browsers: 1287 | if browser.listener == listener: 1288 | browser.cancel() 1289 | del(browser) 1290 | 1291 | def registerService(self, info, ttl=_DNS_TTL): 1292 | """Registers service information to the network with a default TTL 1293 | of 60 seconds. Zeroconf will then respond to requests for 1294 | information for that service. The name of the service may be 1295 | changed if needed to make it unique on the network.""" 1296 | self.checkService(info) 1297 | self.services[info.name.lower()] = info 1298 | if info.type in self.servicetypes: 1299 | self.servicetypes[info.type] += 1 1300 | else: 1301 | self.servicetypes[info.type] = 1 1302 | now = currentTimeMillis() 1303 | nextTime = now 1304 | i = 0 1305 | while i < 3: 1306 | if now < nextTime: 1307 | self.wait(nextTime - now) 1308 | now = currentTimeMillis() 1309 | continue 1310 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1311 | out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, 1312 | _CLASS_IN, ttl, info.name), 0) 1313 | out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, 1314 | _CLASS_IN, ttl, info.priority, info.weight, info.port, 1315 | info.server), 0) 1316 | out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 1317 | ttl, info.text), 0) 1318 | if info.address: 1319 | out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, 1320 | _CLASS_IN, ttl, info.address), 0) 1321 | self.send(out) 1322 | i += 1 1323 | nextTime += _REGISTER_TIME 1324 | 1325 | def unregisterService(self, info): 1326 | """Unregister a service.""" 1327 | try: 1328 | del(self.services[info.name.lower()]) 1329 | if self.servicetypes[info.type] > 1: 1330 | self.servicetypes[info.type] -= 1 1331 | else: 1332 | del self.servicetypes[info.type] 1333 | except: 1334 | pass 1335 | now = currentTimeMillis() 1336 | nextTime = now 1337 | i = 0 1338 | while i < 3: 1339 | if now < nextTime: 1340 | self.wait(nextTime - now) 1341 | now = currentTimeMillis() 1342 | continue 1343 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1344 | out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, 1345 | _CLASS_IN, 0, info.name), 0) 1346 | out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, 1347 | _CLASS_IN, 0, info.priority, info.weight, info.port, 1348 | info.name), 0) 1349 | out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 1350 | 0, info.text), 0) 1351 | if info.address: 1352 | out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, 1353 | _CLASS_IN, 0, info.address), 0) 1354 | self.send(out) 1355 | i += 1 1356 | nextTime += _UNREGISTER_TIME 1357 | 1358 | def unregisterAllServices(self): 1359 | """Unregister all registered services.""" 1360 | if len(self.services) > 0: 1361 | now = currentTimeMillis() 1362 | nextTime = now 1363 | i = 0 1364 | while i < 3: 1365 | if now < nextTime: 1366 | self.wait(nextTime - now) 1367 | now = currentTimeMillis() 1368 | continue 1369 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1370 | for info in self.services.values(): 1371 | out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, 1372 | _CLASS_IN, 0, info.name), 0) 1373 | out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, 1374 | _CLASS_IN, 0, info.priority, info.weight, 1375 | info.port, info.server), 0) 1376 | out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, 1377 | _CLASS_IN, 0, info.text), 0) 1378 | if info.address: 1379 | out.addAnswerAtTime(DNSAddress(info.server, 1380 | _TYPE_A, _CLASS_IN, 0, info.address), 0) 1381 | self.send(out) 1382 | i += 1 1383 | nextTime += _UNREGISTER_TIME 1384 | 1385 | def checkService(self, info): 1386 | """Checks the network for a unique service name, modifying the 1387 | ServiceInfo passed in if it is not unique.""" 1388 | now = currentTimeMillis() 1389 | nextTime = now 1390 | i = 0 1391 | while i < 3: 1392 | for record in self.cache.entriesWithName(info.type): 1393 | if (record.type == _TYPE_PTR and 1394 | not record.isExpired(now) and 1395 | record.alias == info.name): 1396 | if info.name.find('.') < 0: 1397 | info.name = '%s.[%s:%s].%s' % (info.name, 1398 | info.address, info.port, info.type) 1399 | 1400 | self.checkService(info) 1401 | return 1402 | raise NonUniqueNameException 1403 | if now < nextTime: 1404 | self.wait(nextTime - now) 1405 | now = currentTimeMillis() 1406 | continue 1407 | out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA) 1408 | self.debug = out 1409 | out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN)) 1410 | out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR, 1411 | _CLASS_IN, _DNS_TTL, info.name)) 1412 | self.send(out) 1413 | i += 1 1414 | nextTime += _CHECK_TIME 1415 | 1416 | def addListener(self, listener, question): 1417 | """Adds a listener for a given question. The listener will have 1418 | its updateRecord method called when information is available to 1419 | answer the question.""" 1420 | now = currentTimeMillis() 1421 | self.listeners.append(listener) 1422 | if question is not None: 1423 | for record in self.cache.entriesWithName(question.name): 1424 | if question.answeredBy(record) and not record.isExpired(now): 1425 | listener.updateRecord(self, now, record) 1426 | self.notifyAll() 1427 | 1428 | def removeListener(self, listener): 1429 | """Removes a listener.""" 1430 | try: 1431 | self.listeners.remove(listener) 1432 | self.notifyAll() 1433 | except: 1434 | pass 1435 | 1436 | def updateRecord(self, now, rec): 1437 | """Used to notify listeners of new information that has updated 1438 | a record.""" 1439 | for listener in self.listeners: 1440 | listener.updateRecord(self, now, rec) 1441 | self.notifyAll() 1442 | 1443 | def handleResponse(self, msg): 1444 | """Deal with incoming response packets. All answers 1445 | are held in the cache, and listeners are notified.""" 1446 | now = currentTimeMillis() 1447 | for record in msg.answers: 1448 | expired = record.isExpired(now) 1449 | if record in self.cache.entries(): 1450 | if expired: 1451 | self.cache.remove(record) 1452 | else: 1453 | entry = self.cache.get(record) 1454 | if entry is not None: 1455 | entry.resetTTL(record) 1456 | record = entry 1457 | else: 1458 | self.cache.add(record) 1459 | 1460 | self.updateRecord(now, record) 1461 | 1462 | def handleQuery(self, msg, addr, port): 1463 | """Deal with incoming query packets. Provides a response if 1464 | possible.""" 1465 | out = None 1466 | 1467 | # Support unicast client responses 1468 | # 1469 | if port != _MDNS_PORT: 1470 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False) 1471 | for question in msg.questions: 1472 | out.addQuestion(question) 1473 | 1474 | for question in msg.questions: 1475 | if question.type == _TYPE_PTR: 1476 | if question.name == "_services._dns-sd._udp.local.": 1477 | for stype in self.servicetypes: 1478 | if out is None: 1479 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1480 | out.addAnswer(msg, 1481 | DNSPointer("_services._dns-sd._udp.local.", 1482 | _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype)) 1483 | for service in self.services.values(): 1484 | if question.name == service.type: 1485 | if out is None: 1486 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1487 | out.addAnswer(msg, 1488 | DNSPointer(service.type, _TYPE_PTR, 1489 | _CLASS_IN, _DNS_TTL, service.name)) 1490 | else: 1491 | try: 1492 | if out is None: 1493 | out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) 1494 | 1495 | # Answer A record queries for any service addresses we know 1496 | if question.type in (_TYPE_A, _TYPE_ANY): 1497 | for service in self.services.values(): 1498 | if service.server == question.name.lower(): 1499 | out.addAnswer(msg, DNSAddress(question.name, 1500 | _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, 1501 | _DNS_TTL, service.address)) 1502 | 1503 | service = self.services.get(question.name.lower(), None) 1504 | if not service: continue 1505 | 1506 | if question.type in (_TYPE_SRV, _TYPE_ANY): 1507 | out.addAnswer(msg, DNSService(question.name, 1508 | _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, 1509 | _DNS_TTL, service.priority, service.weight, 1510 | service.port, service.server)) 1511 | if question.type in (_TYPE_TXT, _TYPE_ANY): 1512 | out.addAnswer(msg, DNSText(question.name, 1513 | _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, 1514 | _DNS_TTL, service.text)) 1515 | if question.type == _TYPE_SRV: 1516 | out.addAdditionalAnswer(DNSAddress(service.server, 1517 | _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, 1518 | _DNS_TTL, service.address)) 1519 | except: 1520 | traceback.print_exc() 1521 | 1522 | if out is not None and out.answers: 1523 | out.id = msg.id 1524 | self.send(out, addr, port) 1525 | 1526 | def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT): 1527 | """Sends an outgoing packet.""" 1528 | packet = out.packet() 1529 | try: 1530 | while packet: 1531 | bytes_sent = self.socket.sendto(packet, 0, (addr, port)) 1532 | if bytes_sent < 0: 1533 | break 1534 | packet = packet[bytes_sent:] 1535 | except: 1536 | # Ignore this, it may be a temporary loss of network connection 1537 | pass 1538 | 1539 | def close(self): 1540 | """Ends the background threads, and prevent this instance from 1541 | servicing further queries.""" 1542 | global _GLOBAL_DONE 1543 | if not _GLOBAL_DONE: 1544 | _GLOBAL_DONE = True 1545 | self.notifyAll() 1546 | self.engine.notify() 1547 | self.unregisterAllServices() 1548 | self.socket.setsockopt(socket.IPPROTO_IP, 1549 | socket.IP_DROP_MEMBERSHIP, 1550 | socket.inet_aton(_MDNS_ADDR) + 1551 | socket.inet_aton('0.0.0.0')) 1552 | self.socket.close() 1553 | 1554 | # Test a few module features, including service registration, service 1555 | # query (for Zoe), and service unregistration. 1556 | 1557 | if __name__ == '__main__': 1558 | print("Multicast DNS Service Discovery for Python, version %s" % 1559 | __version__) 1560 | r = Zeroconf() 1561 | print("1. Testing registration of a service...") 1562 | desc = {'version':'0.10','a':'test value', 'b':'another value'} 1563 | info = ServiceInfo("_http._tcp.local.", 1564 | "My Service Name._http._tcp.local.", 1565 | socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc) 1566 | print(" Registering service...") 1567 | r.registerService(info) 1568 | print(" Registration done.") 1569 | print("2. Testing query of service information...") 1570 | print(" Getting ZOE service:") 1571 | print(str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))) 1572 | print(" Query done.") 1573 | print("3. Testing query of own service...") 1574 | print(" Getting self:") 1575 | print(str(r.getServiceInfo("_http._tcp.local.", 1576 | "My Service Name._http._tcp.local."))) 1577 | print(" Query done.") 1578 | print("4. Testing unregister of service information...") 1579 | r.unregisterService(info) 1580 | print(" Unregister done.") 1581 | r.close() 1582 | -------------------------------------------------------------------------------- /zunittest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Unit tests for zeroconf.py """ 4 | 5 | import zeroconf as r 6 | import struct 7 | import unittest 8 | 9 | class PacketGeneration(unittest.TestCase): 10 | 11 | def testParseOwnPacketSimple(self): 12 | generated = r.DNSOutgoing(0) 13 | parsed = r.DNSIncoming(generated.packet()) 14 | 15 | def testParseOwnPacketSimpleUnicast(self): 16 | generated = r.DNSOutgoing(0, 0) 17 | parsed = r.DNSIncoming(generated.packet()) 18 | 19 | def testParseOwnPacketFlags(self): 20 | generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) 21 | parsed = r.DNSIncoming(generated.packet()) 22 | 23 | def testParseOwnPacketQuestion(self): 24 | generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) 25 | generated.addQuestion(r.DNSQuestion("testname.local.", r._TYPE_SRV, 26 | r._CLASS_IN)) 27 | parsed = r.DNSIncoming(generated.packet()) 28 | 29 | def testMatchQuestion(self): 30 | generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) 31 | question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) 32 | generated.addQuestion(question) 33 | parsed = r.DNSIncoming(generated.packet()) 34 | self.assertEqual(len(generated.questions), 1) 35 | self.assertEqual(len(generated.questions), len(parsed.questions)) 36 | self.assertEqual(question, parsed.questions[0]) 37 | 38 | class PacketForm(unittest.TestCase): 39 | 40 | def testTransactionID(self): 41 | """ID must be zero in a DNS-SD packet""" 42 | generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) 43 | bytes = generated.packet() 44 | id = r.getByte(bytes[0]) << 8 | r.getByte(bytes[1]) 45 | self.assertEqual(id, 0) 46 | 47 | def testQueryHeaderBits(self): 48 | generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) 49 | bytes = generated.packet() 50 | flags = r.getByte(bytes[2]) << 8 | r.getByte(bytes[3]) 51 | self.assertEqual(flags, 0x0) 52 | 53 | def testResponseHeaderBits(self): 54 | generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) 55 | bytes = generated.packet() 56 | flags = r.getByte(bytes[2]) << 8 | r.getByte(bytes[3]) 57 | self.assertEqual(flags, 0x8000) 58 | 59 | def testNumbers(self): 60 | generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) 61 | bytes = generated.packet() 62 | (numQuestions, numAnswers, numAuthorities, 63 | numAdditionals) = struct.unpack('!4H', bytes[4:12]) 64 | self.assertEqual(numQuestions, 0) 65 | self.assertEqual(numAnswers, 0) 66 | self.assertEqual(numAuthorities, 0) 67 | self.assertEqual(numAdditionals, 0) 68 | 69 | def testNumbersQuestions(self): 70 | generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) 71 | question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) 72 | for i in range(10): 73 | generated.addQuestion(question) 74 | bytes = generated.packet() 75 | (numQuestions, numAnswers, numAuthorities, 76 | numAdditionals) = struct.unpack('!4H', bytes[4:12]) 77 | self.assertEqual(numQuestions, 10) 78 | self.assertEqual(numAnswers, 0) 79 | self.assertEqual(numAuthorities, 0) 80 | self.assertEqual(numAdditionals, 0) 81 | 82 | class Names(unittest.TestCase): 83 | 84 | def testLongName(self): 85 | generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) 86 | question = r.DNSQuestion("this.is.a.very.long.name.with.lots.of.parts.in.it.local.", 87 | r._TYPE_SRV, r._CLASS_IN) 88 | generated.addQuestion(question) 89 | parsed = r.DNSIncoming(generated.packet()) 90 | 91 | def testExceedinglyLongName(self): 92 | generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) 93 | name = "%slocal." % ("part." * 1000) 94 | question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) 95 | generated.addQuestion(question) 96 | parsed = r.DNSIncoming(generated.packet()) 97 | 98 | def testExceedinglyLongNamePart(self): 99 | name = "%s.local." % ("a" * 1000) 100 | generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) 101 | question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) 102 | generated.addQuestion(question) 103 | self.assertRaises(r.NamePartTooLongException, generated.packet) 104 | 105 | def testSameName(self): 106 | name = "paired.local." 107 | generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) 108 | question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN) 109 | generated.addQuestion(question) 110 | generated.addQuestion(question) 111 | parsed = r.DNSIncoming(generated.packet()) 112 | 113 | class Framework(unittest.TestCase): 114 | 115 | def testLaunchAndClose(self): 116 | rv = r.Zeroconf() 117 | rv.close() 118 | 119 | if __name__ == '__main__': 120 | unittest.main() 121 | -------------------------------------------------------------------------------- /zwebbrowse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Example of browsing for a service (in this case, HTTP) """ 4 | 5 | from zeroconf import * 6 | import socket 7 | import time 8 | 9 | class MyListener(object): 10 | def __init__(self): 11 | self.r = Zeroconf() 12 | 13 | def removeService(self, zeroconf, type, name): 14 | print('') 15 | print("Service %s removed" % name) 16 | 17 | def addService(self, zeroconf, type, name): 18 | print('') 19 | print("Service %s added" % name) 20 | print(" Type is %s" % type) 21 | info = self.r.getServiceInfo(type, name) 22 | if info: 23 | print(" Address is %s:%d" % (socket.inet_ntoa(info.getAddress()), 24 | info.getPort())) 25 | print(" Weight is %d, Priority is %d" % (info.getWeight(), 26 | info.getPriority())) 27 | print(" Server is %s" % info.getServer()) 28 | prop = info.getProperties() 29 | if prop: 30 | print(" Properties are") 31 | for key, value in prop.items(): 32 | print(" %s: %s" % (key, value)) 33 | 34 | if __name__ == '__main__': 35 | print("Multicast DNS Service Discovery for Python Browser test") 36 | r = Zeroconf() 37 | print("Testing browsing for a service...") 38 | type = "_http._tcp.local." 39 | listener = MyListener() 40 | browser = ServiceBrowser(r, type, listener) 41 | time.sleep(5) 42 | r.close() 43 | -------------------------------------------------------------------------------- /zwebtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Example of announcing a service (in this case, a fake HTTP server) """ 4 | 5 | from zeroconf import * 6 | import socket 7 | 8 | desc = {'path': '/~paulsm/'} 9 | 10 | info = ServiceInfo("_http._tcp.local.", 11 | "Paul's Test Web Site._http._tcp.local.", 12 | socket.inet_aton("10.0.1.2"), 80, 0, 0, 13 | desc, "ash-2.local.") 14 | 15 | r = Zeroconf() 16 | print("Registration of a service...") 17 | r.registerService(info) 18 | waiting = "Waiting (press Enter to exit)..." 19 | if pythree: 20 | input(waiting) 21 | else: 22 | raw_input(waiting) 23 | print("Unregistering...") 24 | r.unregisterService(info) 25 | r.close() 26 | --------------------------------------------------------------------------------