pax_global_header00006660000000000000000000000064124420474450014520gustar00rootroot0000000000000052 comment=4838af158cd02b80be78cc9972d87bf00e953da4 pyzor-release-1-0-0/000077500000000000000000000000001244204744500143155ustar00rootroot00000000000000pyzor-release-1-0-0/.gitignore000066400000000000000000000001371244204744500163060ustar00rootroot00000000000000*.pyc /build/ /dist/ /docs/.build/ /pyzor.egg-info/ /.project /.pydevproject /.idea /.settings pyzor-release-1-0-0/.travis.yml000066400000000000000000000004211244204744500164230ustar00rootroot00000000000000language: python python: - "2.7" - "3.4" - "pypy" - "pypy3" services: - redis-server install: - "./scripts/run_tests prepare" script: - "py.test tests/unit/ --cov pyzor --cov-report term-missing" - "py.test tests/functional/" after_success: - coveralls pyzor-release-1-0-0/COPYING000066400000000000000000000431271244204744500153570ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. pyzor-release-1-0-0/INSTALL000066400000000000000000000006721244204744500153530ustar00rootroot00000000000000For more details and documentation on installing and using pyzor please visit http://www.pyzor.org/ Pyzor requires at least Python 2.6 To install this distribution, simply run the following: python setup.py build python setup.py install Pyzor also works with Python3.3. The code will be automatically refactored with 2to3 during the setup: python3.3 setup.py install Note that the MySQLdb library does not currently support Python3. pyzor-release-1-0-0/MANIFEST.in000066400000000000000000000002521244204744500160520ustar00rootroot00000000000000include COPYING include INSTALL include THANKS include requirements.txt include config/* include pyzor/* include pyzor/engines/* include pyzor/hacks/* include scripts/* pyzor-release-1-0-0/README.rst000066400000000000000000000016721244204744500160120ustar00rootroot00000000000000.. image:: /docs/.static/pyzor.gif?raw=true :target: http://www.pyzor.org/ :alt: Pyzor Pyzor is a Python implementation of a spam-blocking networked system that use spam signatures to identify them. .. image:: https://pypip.in/v/pyzor/badge.png :target: https://pypi.python.org/pypi/pyzor/ :alt: Latest PyPI version .. image:: https://pypip.in/d/pyzor/badge.png :target: https://pypi.python.org/pypi/pyzor/ :alt: Number of PyPI downloads .. image:: https://travis-ci.org/SpamExperts/pyzor.svg?branch=master :target: https://travis-ci.org/SpamExperts/pyzor :alt: Build status .. image:: https://coveralls.io/repos/SpamExperts/pyzor/badge.png?branch=master :target: https://coveralls.io/r/SpamExperts/pyzor?branch=master Quick links: * `Documentation `_ * `Download `_ * `Issue Tracker `_ pyzor-release-1-0-0/THANKS000066400000000000000000000011001244204744500152200ustar00rootroot00000000000000Pyzor was originally written by Frank Tobin. Other people contributed by reporting problems, suggesting various improvements or submitting actual code. Here is a list of those people. Help me keep it complete and free of errors. Frank Tobin ftobin@neverending.org Rick Macdougall rickm@nougen.com Colin Smith colin@archeus.plus.com Bobby Rose brose@med.wayne.edu Roman Suzi rnd@onego.ru Robert Schiele schiele@users.sourceforge.net Tobias Klauser tux_edo@users.sourceforge.net Tony Meyer tony.meyer@gmail.com Alexandru Chirila chirila.s.alexandru@gmail.com pyzor-release-1-0-0/config/000077500000000000000000000000001244204744500155625ustar00rootroot00000000000000pyzor-release-1-0-0/config/accounts.sample000066400000000000000000000004471244204744500206110ustar00rootroot00000000000000## This file should contain a list of `host : port : username : salt,key` ## each on a new line. The salt and key can be generated with genkey command ## in the pyzor client. Example: # 127.0.0.1 : 24441 : alice : d28f86151e80a9accba4a4eba81c460532384cd6,fc7f1cad729b5f3862b2ef192e2d9e0d0d4bd515pyzor-release-1-0-0/config/config.sample000066400000000000000000000054241244204744500202370ustar00rootroot00000000000000## Note that the options that require a file name, must not contain absolute ## paths. They are relative to the specified --homedir, which defaults to ## ~/.pyzor ## All of these options are overridable from the respective command-line ## arguments. ## The client section only affects the pyzor client. [client] ## The `ServersFile` must contain a newline-separated list of server ## addresses to report/whitelist/check with. # ServersFile = servers ## The `AccountsFile` file containing information about accounts on servers. # AccountsFile = accounts ## This option specifies the name of the log file. # LogFile = ## The `LocaWhitelist` file containing skipped digests. # LocalWhitelist = whitelist ## This options specifies the number of seconds that the pyzor client should ## wait for a response from the server before timing out. # Timeout = 5 ## This options specifies the input style of the pyzor client. Current options ## are: ## - msg (individual RFC5321 message) ## - mbox (mbox file of messages) ## - digests (Pyzor digests, one per line) # Style = msg ## Thes options specify the threshold for number of reports/whitelists. ## According to these thresholds the pyzor client exit code will differ. # ReportThreshold = 0 # WhitelistThreshold = 0 ## The server section only affects the pyzord server. [server] ## Specifes the port and interface to listen on. # Port = 24441 # ListenAddress = 0.0.0.0 ## This option specifies the name of the log file. # LogFile = ## This option specifies the name of the usage log file. # UsageLogFile = ## This file will contain the PID of the pyzord daemon, when the it's ## started with the --detach options. The file is removed when the daemon is ## closed # PidFile = pyzord.pid ## This file must contain the username and their keys # PasswdFile = pyzord.passwd ## This file defines the ACL for the users # AccessFile = pyzord.access ## If set to True then use the gevent library. # Gevent = False ## These settings define the storage engine that the pyzord server should use. ## Example for gdbm (default): # Engine = gdbm # DigestDB = pyzord.db ## Example for mysql: # Engine = mysql # DigestDB = localhost,user,passwd,pyzor_db,pyzor_table ## Example for redis: # Engine = redis # DigestDB = localhost,6379,,0 ## Or if a password is required # DigestDB = localhost,6379,passwd,0 ## The maximum age of an record, after which it will be removed. ## To disable this set this to 0. # CleanupAge = 10368000 # aprox 4 months ## These setting define how and if the pyzord server should use concurrency ## For pre-forking # PreFork = 0 # disabled ## For multi-threading: # Threads = False # MaxThreads = 0 # unlimited # DBConnections = 0 # new connection for each request ## For multi-processing: # Processes = False # MaxProcesses = 40 pyzor-release-1-0-0/config/pyzord.access.sample000066400000000000000000000004121244204744500215510ustar00rootroot00000000000000## This defines the ACL for each user, by default if a user is not specified ## here he is denied all access ( this includes anonymous users). Examples: # check report ping pong info whitelist : alice : allow # ALL : anonymous : allow # whitelist : anonymous : denypyzor-release-1-0-0/config/pyzord.paswd.sample000066400000000000000000000003441244204744500214320ustar00rootroot00000000000000## This file must contain the username and their keys, so that the recieving ## server can verify the user's signature. Example # alice : fc7f1cad729b5f3862b2ef192e2d9e0d0d4bd515 # bob : cf88277c5d4abdc0a3f56f416011966d04a3f462pyzor-release-1-0-0/config/servers.sample000066400000000000000000000002211244204744500204510ustar00rootroot00000000000000## This file should contain a list of pyzor servers to which to direct the ## requests. Each address:port on a new line. public.pyzor.org:24441pyzor-release-1-0-0/docs/000077500000000000000000000000001244204744500152455ustar00rootroot00000000000000pyzor-release-1-0-0/docs/.static/000077500000000000000000000000001244204744500166125ustar00rootroot00000000000000pyzor-release-1-0-0/docs/.static/pyzor.gif000066400000000000000000000115031244204744500204640ustar00rootroot00000000000000GIF89a,ܙVˬUFxiĪŦd-3еd}۷ɂɎ7jxQwסLǫyϨCsw鳳o7jxSQĽ觾Ӯ?7j͙ˆɀћ࡚tŲn˳xw*̾歬:ռ䭬©7jټµ7ja`Ɣcڴyx󡡤TRӰͺ֊áykiϼo렟ˢjݹ]\법7jRwᄛǞѯ_<:ǀ߲𺐏n7j]כⷷї?nʎ֘s:X߱`ȔɖߕܹϹǹȬֺݹ߼neDpӨۦqȐ״7j!,, H*\Ȱ*QcË3jQ?4`Ɋ:\ɲe>%<_HTҥϟ@3@VAU U`C]2Co4x^HT۷p6-̷s25b ̧C>8a G`ɗ!C'h~T1s~L@cӨQWB#Ϻ+ШbڅgcA ރf:RΑ"33tOړ 2N>łK0*PL˟O?%d|&(aUR` 6`:| U3N=0  T< D| 5,0T LDIn#شw(n0K%> WDd(dPaihy BppܖM$ 9A"Q1 19i6`Cd_A> %t駞ÐjD <& @Oa\0+{A {05d!5tDg7p 3$0Ůd@M泥.<14@ʂEP *8 wĔ 1jZ# P<#dV l8aq%T%N3 ,lG7%h~Ե~}@ CCG0B@7bpֆMd:ubaBg($h.p B p< ,p)#8!n3Xa!Uo$L? )5PmH,>ZEB`o"#X6L b.qRÏ~+(@%p ArpDUrH-a8ьM؁)h 1YZܰ4pЫ s%jL$.*A; "0IF57Ml c fgAc;0F̡(- )P !CQBAL1@ p 8!# :ƀH8@`G QD}$3Xj6[crx0<eJaIB)L gx[*]@G  7xGwL-V*?Sc(Fłzna\R!BPYsdZ;Oو`hPc-,cXB zaG= 0ԁ%!ԡD( PۂmuK\.؈,9a Q@Vj0 `1B;j~H2;k*[ORX=8hBЂD^B7 Qnlz 0ѱZBkCpғ\4Lmʁ#`"h4NRp`ed*egN!mÇ > 1pB ~$e6pDa^C Bx 3*0=\$цjdĨ̈#d5p%15I;bH@T|7v9obTP V/FPuүL yPz`0T 0 y ` `W=9 mS= j!R@ɟ~ :0&AS5{p p0@50p0P 0E y=` tpr^#* xt P Q,ZfkwY Ԡ6zpd P0i0 b yPZ7"D j/ PR`@ 0@+p'`K e}ڟ| fP??p `|@P y0"7mp Q_6ٟ0T/`b ;4+p 4tFQ _` 09ô O6pAp k!*x5 (p #9,4<  NH_* Yl~nAA[s+5+ő?@ B`ܟZs4'n|Ȃ È | k1' fL;pyzor-release-1-0-0/docs/Makefile000066400000000000000000000151461244204744500167140ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = .build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pyzor.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pyzor.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Pyzor" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pyzor" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." pyzor-release-1-0-0/docs/about.rst000066400000000000000000000027521244204744500171170ustar00rootroot00000000000000About ====== History -------- Pyzor initially started out to be merely a Python implementation of Razor, but due to the protocol and the fact that Razor's server is not Open Source or software libre, Frank Tobin decided to implement Pyzor with a new protocol and release the entire system as Open Source and software libre. Protocol ---------- The central premise of Pyzor is that it converts an email message to a short digest that uniquely identifies the message. Simply hashing the entire message is an ineffective method of generating a digest, because message headers will differ when the content does not, and because spammers will often try to make a message unique by injecting random/unrelated text into their messages. To generate a digest, the 2.0 version of the Pyzor protocol: * Discards all message headers. * If the message is greater than 4 lines in length: * Discards the first 20% of the message. * Uses the next 3 lines. * Discards the next 40% of the message. * Uses the next 3 lines. * Discards the remainder of the message. * Removes any 'words' (sequences of characters separated by whitespace) that are 10 or more characters long. * Removes anything that looks like an email address (X@Y). * Removes anything that looks like a URL. * Removes anything that looks like HTML tags. * Removes any whitespace. * Discards any lines that are fewer than 8 characters in length. This is intended as an easy-to-understand explanation, rather than a technical one. pyzor-release-1-0-0/docs/accounts.rst000066400000000000000000000033731244204744500176240ustar00rootroot00000000000000Accounts ========== Pyzor Accounts can be used to grant or restrict access to the Pyzor Server, by ensuring the client are authenticated. To get an account on a server requires coordination between the client user and server admin. Use the following steps: #. User and admin should agree on a username for the user. Allowed characters for a username are alpha-numerics, the underscore, and dashes. The normative regular expression it must match is ``^[-\.\w]+$``. Let us assume they have agreed on *bob*. #. User generates a key with ``pyzor genkey``. Let us say that it generates the salt,key of:: 227bfb58efaba7c582d9dcb66ab2063d38df2923,8da9f54058c34e383e997f45d6eb74837139f83b #. Assuming the server is at ``127.0.0.1:9999``, the user puts the following entry into ``~/.pyzor/accounts``:: 127.0.0.1 : 9999 : bob : 227bfb58efaba7c582d9dcb66ab2063d38df2923,8da9f54058c34e383e997f45d6eb74837139f83b This tells the Pyzor Client to use the *bob* account for server ``127.0.0.1:9999``. It will still use the *anonymous* user for all other servers. #. The user then sends the key (the part to the right-hand side of the comma) to the admin. #. The admin adds the key to their ``~/.pyzor/pyzord.passwd``:: bob : 8da9f54058c34e383e997f45d6eb74837139f83b #. Assuming the admin wants to give the privilege of whitelisting (in addition to the normal permissions), the admin then adds the appropriate permissions to ``~/.pyzor/pyzord.access``:: check report ping pong info whitelist : bob : allow For more information see :ref:`server-access-file`. #. To reload the account and access information send the ``USR1`` signal to the daemon.pyzor-release-1-0-0/docs/changelog.rst000066400000000000000000000116401244204744500177300ustar00rootroot00000000000000Changelog =========== Pyzor 1.0.0 ------------ New features: * New pyzor commands ``local_[un]whitelist`` are available for managing a local whitelist on the client side. (`#10 `_) * New ``PreFork`` option for the pyzor server. This allows creating multiple workers for handling pyzor requests. (`#26 `_) Perfomance enhancements: * Improve usage of the Redis engine by using Hashes instead of string for storing digests. The migration tool can be used to update you current database. (`#29 `_) Others: * PyPy3 compatibility verified and introduced into the Travis-CI system. (`#24 `_) * Unification of the storage engines types. (`#30 `_) * Improved check on the public whitelisting request service to skip sending requests to whitelist message that have not been reported to the public database or have been already whitelisted. (`#27 `_) Pyzor 0.9.0 ------------ Bug fixes: * Fix gdbm decoding issue. (`#20 `_) * Fix inconsistencies accounts and addresses. (`#22 `_) New features: * Strip content inside ``
This is a test.
--001a11c25ff293069304f0126bfd-- """ TEXT_ATTACHMENT = """MIME-Version: 1.0 Received: by 10.76.127.40 with HTTP; Fri, 17 Jan 2014 02:21:43 -0800 (PST) Date: Fri, 17 Jan 2014 12:21:43 +0200 Delivered-To: chirila.s.alexandru@gmail.com Message-ID: Subject: Test From: Alexandru Chirila To: Alexandru Chirila Content-Type: multipart/mixed; boundary=f46d040a62c49bb1c804f027e8cc --f46d040a62c49bb1c804f027e8cc Content-Type: multipart/alternative; boundary=f46d040a62c49bb1c404f027e8ca --f46d040a62c49bb1c404f027e8ca Content-Type: text/plain; charset=ISO-8859-1 This is a test mailing --f46d040a62c49bb1c404f027e8ca-- --f46d040a62c49bb1c804f027e8cc Content-Type: image/png; name="tar.png" Content-Disposition: attachment; filename="tar.png" Content-Transfer-Encoding: base64 X-Attachment-Id: f_hqjas5ad0 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 QmCC --f46d040a62c49bb1c804f027e8cc--""" class PyzorPreDigestTest(PyzorTestBase): # we don't need the pyzord server to test this @classmethod def setUpClass(cls): pass @classmethod def tearDownClass(cls): pass def setUp(self): # no argument necessary self.client_args = {} def test_predigest_email(self): """Test email removal in the predigest process""" emails = ["t@abc.ro", "t1@abc.ro", "t+@abc.ro", "t.@abc.ro", ] message = "Test %s Test2" expected = b"TestTest2\n" for email in emails: msg = message % email res = self.check_pyzor("predigest", None, input=TEXT % msg) self.assertEqual(res, expected) def test_predigest_long(self): """Test long "words" removal in the predigest process""" strings = ["0A2D3f%a#S", "3sddkf9jdkd9", "@@#@@@@@@@@@"] message = "Test %s Test2" expected = b"TestTest2\n" for s in strings: msg = message % s res = self.check_pyzor("predigest", None, input=TEXT % msg) self.assertEqual(res, expected) def test_predigest_line_length(self): """Test small lines removal in the predigest process""" msg = "This line is included\n"\ "not this\n"\ "This also" expected = b"Thislineisincluded\nThisalso\n" res = self.check_pyzor("predigest", None, input=TEXT % msg) self.assertEqual(res, expected) def test_predigest_atomic(self): """Test atomic messages (lines <= 4) in the predigest process""" msg = "All this message\nShould be included\nIn the predigest" expected = b"Allthismessage\nShouldbeincluded\nInthepredigest\n" res = self.check_pyzor("predigest", None, input=TEXT % msg) self.assertEqual(res, expected) def test_predigest_pieced(self): """Test pieced messages (lines > 4) in the predigest process""" msg = "" for i in range(100): msg += "Line%d test test test\n" % i expected = b"" for i in [20, 21, 22, 60, 61, 62]: expected += ("Line%dtesttesttest\n" % i).encode("utf8") res = self.check_pyzor("predigest", None, input=TEXT % msg) self.assertEqual(res, expected) def test_predigest_html(self): expected = """Emailspam,alsoknownasjunkemailorbulkemail,isasubset ofspaminvolvingnearlyidenticalmessagessenttonumerous byemail.Clickingonlinksinspamemailmaysendusersto byemail.Clickingonlinksinspamemailmaysendusersto phishingwebsitesorsitesthatarehostingmalware. Emailspam.Emailspam,alsoknownasjunkemailorbulkemail,isasubsetofspaminvolvingnearlyidenticalmessagessenttonumerousbyemail.Clickingonlinksinspamemailmaysenduserstophishingwebsitesorsitesthatarehostingmalware. """.encode("utf8") res = self.check_pyzor("predigest", None, input=HTML_TEXT) self.assertEqual(res, expected) def test_predigest_html_style_script(self): expected = """Thisisatest. Thisisatest. """.encode("utf8") res = self.check_pyzor("predigest", None, input=HTML_TEXT_STYLE_SCRIPT) self.assertEqual(res, expected) def test_predigest_attachemnt(self): expected = b"Thisisatestmailing\n" res = self.check_pyzor("predigest", None, input=TEXT_ATTACHMENT) self.assertEqual(res, expected) class PyzorDigestTest(PyzorTestBase): # we don't need the pyzord server to test this @classmethod def setUpClass(cls): pass @classmethod def tearDownClass(cls): pass def setUp(self): # no argument necessary self.client_args = {} def test_digest_email(self): """Test email removal in the digest process""" emails = ["t@abc.ro", "t1@abc.ro", "t+@abc.ro", "t.@abc.ro", ] message = "Test %s Test2" expected = b"TestTest2" for email in emails: msg = message % email res = self.check_pyzor("digest", None, input=TEXT % msg) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") def test_digest_long(self): """Test long "words" removal in the digest process""" strings = ["0A2D3f%a#S", "3sddkf9jdkd9", "@@#@@@@@@@@@"] message = "Test %s Test2" expected = b"TestTest2" for s in strings: msg = message % s res = self.check_pyzor("digest", None, input=TEXT % msg) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") def test_digest_line_length(self): """Test small lines removal in the digest process""" msg = "This line is included\n"\ "not this\n"\ "This also" expected = b"ThislineisincludedThisalso" res = self.check_pyzor("digest", None, input=TEXT % msg) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") def test_digest_atomic(self): """Test atomic messages (lines <= 4) in the digest process""" msg = "All this message\nShould be included\nIn the digest" expected = b"AllthismessageShouldbeincludedInthedigest" res = self.check_pyzor("digest", None, input=TEXT % msg) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") def test_digest_pieced(self): """Test pieced messages (lines > 4) in the digest process""" msg = "" for i in range(100): msg += "Line%d test test test\n" % i expected = b"" for i in [20, 21, 22, 60, 61, 62]: expected += ("Line%dtesttesttest" % i).encode("utf8") res = self.check_pyzor("digest", None, input=TEXT % msg) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") def test_digest_html(self): expected = """Emailspam,alsoknownasjunkemailorbulkemail,isasubset ofspaminvolvingnearlyidenticalmessagessenttonumerous byemail.Clickingonlinksinspamemailmaysendusersto byemail.Clickingonlinksinspamemailmaysendusersto phishingwebsitesorsitesthatarehostingmalware. Emailspam.Emailspam,alsoknownasjunkemailorbulkemail,isasubsetofspaminvolvingnearlyidenticalmessagessenttonumerousbyemail.Clickingonlinksinspamemailmaysenduserstophishingwebsitesorsitesthatarehostingmalware. """.replace("\n", "").encode("utf8") res = self.check_pyzor("digest", None, input=HTML_TEXT) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") def test_digest_html_style_script(self): expected = """Thisisatest.Thisisatest.""".encode("utf8") res = self.check_pyzor("digest", None, input=HTML_TEXT_STYLE_SCRIPT) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") def test_digest_attachemnt(self): expected = b"Thisisatestmailing" res = self.check_pyzor("digest", None, input=TEXT_ATTACHMENT) self.assertEqual(res.decode("utf8"), hashlib.sha1(expected).hexdigest().lower() + "\n") ENCODING_TEST_EMAIL = """From nobody Tue Apr 1 13:18:54 2014 Content-Type: multipart/related; boundary="===============0632694142025794937==" MIME-Version: 1.0 This is a multi-part message in MIME format. --===============0632694142025794937== Content-Type: text/plain; charset="iso-8859-1" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Thist is a t=E9st --===============0632694142025794937== MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64 VGhpcyBpcyBhIHRlc3Qg5r+A5YWJ6YCZ --===============0632694142025794937== MIME-Version: 1.0 Content-Type: text/plain; charset="cp1258" Content-Transfer-Encoding: base64 VGhpcyBpcyBhIHTpc3Qg4qXG --===============0632694142025794937==-- """ BAD_ENCODING = """From nobody Tue Apr 1 13:18:54 2014 Content-Type: multipart/related; boundary="===============0632694142025794937==" MIME-Version: 1.0 This is a multi-part message in MIME format. --===============0632694142025794937== Content-Type: text/plain; charset=ISO-8859-1Content-Transfer-Encoding: quoted-printable This is a test --===============0632694142025794937== Content-Type: text/plain; charset=us-asciia Content-Transfer-Encoding: quoted-printable This is a test --===============0632694142025794937== """ class PyzorEncodingTest(PyzorTestBase): # we don't need the pyzord server to test this @classmethod def setUpClass(cls): pass @classmethod def tearDownClass(cls): pass def setUp(self): # no argument necessary self.client_args = {} def test_encodings(self): expected = "47a83cd0e5cc9bd2c64c06c00e3853f79e63014f\n" res = self.check_pyzor("digest", None, input=ENCODING_TEST_EMAIL) self.assertEqual(res.decode("utf8"), expected) def test_bad_encoding(self): expected = "2b4dbf2fb521edd21d997f3f04b1c7155ba91fff\n" res = self.check_pyzor("digest", None, input=BAD_ENCODING) self.assertEqual(res.decode("utf8"), expected) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(PyzorDigestTest)) test_suite.addTest(unittest.makeSuite(PyzorPreDigestTest)) test_suite.addTest(unittest.makeSuite(PyzorEncodingTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/functional/test_engines/000077500000000000000000000000001244204744500223105ustar00rootroot00000000000000pyzor-release-1-0-0/tests/functional/test_engines/__init__.py000066400000000000000000000013511244204744500244210ustar00rootroot00000000000000"""A suite of functional tests that verifies the correct behaviour of the pyzor client and server as a whole. Functional test should not touch real data and are usually safe, but it's not recommended to run theses on production servers. Note these tests the installed version of pyzor, not the version from the source. """ import unittest def suite(): """Gather all the tests from this package in a test suite.""" import test_gdbm import test_mysql import test_redis test_suite = unittest.TestSuite() test_suite.addTest(test_gdbm.suite()) test_suite.addTest(test_mysql.suite()) test_suite.addTest(test_redis.suite()) return test_suite if __name__ == '__main__': unittest.main(defaultTest='suite') pyzor-release-1-0-0/tests/functional/test_engines/test_gdbm.py000066400000000000000000000017611244204744500246370ustar00rootroot00000000000000import unittest from tests.util import * try: import gdbm has_gdbm = True except ImportError: has_gdbm = False @unittest.skipIf(not has_gdbm, "gdbm library not available") class GdbmPyzorTest(PyzorTest, PyzorTestBase): """Test the gdbm engine""" dsn = "pyzord.db" engine = "gdbm" class ThreadsGdbmPyzorTest(GdbmPyzorTest): """Test the gdbm engine with threads activated.""" threads = "True" max_threads = "0" class MaxThreadsGdbmPyzorTest(GdbmPyzorTest): """Test the gdbm engine with with maximum threads.""" threads = "True" max_threads = "10" def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(GdbmPyzorTest)) test_suite.addTest(unittest.makeSuite(ThreadsGdbmPyzorTest)) test_suite.addTest(unittest.makeSuite(MaxThreadsGdbmPyzorTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/functional/test_engines/test_mysql.py000066400000000000000000000075161244204744500250770ustar00rootroot00000000000000import unittest import ConfigParser from tests.util import * try: import MySQLdb has_mysql = True except ImportError: has_mysql = False schema = """ CREATE TABLE IF NOT EXISTS `%s` ( `digest` char(40) default NULL, `r_count` int(11) default NULL, `wl_count` int(11) default NULL, `r_entered` datetime default NULL, `wl_entered` datetime default NULL, `r_updated` datetime default NULL, `wl_updated` datetime default NULL, PRIMARY KEY (`digest`) ) """ @unittest.skipIf(not os.path.exists("./test.conf"), "test.conf is not available") @unittest.skipIf(not has_mysql, "MySQLdb library not available") class MySQLdbPyzorTest(PyzorTest, PyzorTestBase): """Test the mysql engine.""" dsn = None engine = "mysql" @classmethod def setUpClass(cls): conf = ConfigParser.ConfigParser() conf.read("./test.conf") table = conf.get("test", "table") db = MySQLdb.Connect(host=conf.get("test", "host"), user=conf.get("test", "user"), passwd=conf.get("test", "passwd"), db=conf.get("test", "db")) c = db.cursor() c.execute(schema % table) c.close() db.close() cls.dsn = "%s,%s,%s,%s,%s" % (conf.get("test", "host"), conf.get("test", "user"), conf.get("test", "passwd"), conf.get("test", "db"), conf.get("test", "table")) super(MySQLdbPyzorTest, cls).setUpClass() @classmethod def tearDownClass(cls): super(MySQLdbPyzorTest, cls).tearDownClass() try: conf = ConfigParser.ConfigParser() conf.read("./test.conf") table = conf.get("test", "table") db = MySQLdb.Connect(host=conf.get("test", "host"), user=conf.get("test", "user"), passwd=conf.get("test", "passwd"), db=conf.get("test", "db")) c = db.cursor() c.execute("DROP TABLE %s" % table) c.close() db.close() except: pass class ThreadsMySQLdbPyzorTest(MySQLdbPyzorTest): """Test the mysql engine with threads activated.""" threads = "True" max_threads = "0" class BoundedThreadsMySQLdbPyzorTest(MySQLdbPyzorTest): """Test the mysql engine with threads and DBConnections set.""" threads = "True" max_threads = "0" db_connections = "10" class MaxThreadsMySQLdbPyzorTest(MySQLdbPyzorTest): """Test the mysql engine with threads and MaxThreads set.""" threads = "True" max_threads = "10" class BoundedMaxThreadsMySQLdbPyzorTest(MySQLdbPyzorTest): """Test the mysql engine with threads, MaxThreads and DBConnections set.""" threads = "True" max_threads = "10" db_connections = "10" class ProcessesMySQLdbPyzorTest(MySQLdbPyzorTest): processes = "True" max_processes = "10" class PreForkMySQLdbPyzorTest(MySQLdbPyzorTest): prefork = "4" def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(MySQLdbPyzorTest)) test_suite.addTest(unittest.makeSuite(ThreadsMySQLdbPyzorTest)) test_suite.addTest(unittest.makeSuite(BoundedThreadsMySQLdbPyzorTest)) test_suite.addTest(unittest.makeSuite(MaxThreadsMySQLdbPyzorTest)) test_suite.addTest(unittest.makeSuite(BoundedMaxThreadsMySQLdbPyzorTest)) test_suite.addTest(unittest.makeSuite(ProcessesMySQLdbPyzorTest)) test_suite.addTest(unittest.makeSuite(PreForkMySQLdbPyzorTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/functional/test_engines/test_redis.py000066400000000000000000000024321244204744500250300ustar00rootroot00000000000000import unittest try: import redis has_redis = True except ImportError: has_redis = False from tests.util import * @unittest.skipIf(not has_redis, "redis library not available") class RedisPyzorTest(PyzorTest, PyzorTestBase): """Test the redis engine""" dsn = "localhost,,,10" engine = "redis" @classmethod def tearDownClass(cls): super(RedisPyzorTest, cls).tearDownClass() redis.StrictRedis(db=10).flushdb() class ThreadsRedisPyzorTest(RedisPyzorTest): """Test the redis engine with threads activated.""" threads = "True" class MaxThreadsRedisPyzorTest(RedisPyzorTest): """Test the gdbm engine with with maximum threads.""" threads = "True" max_threads = "10" class PreForkRedisPyzorTest(RedisPyzorTest): """Test the redis engine with threads activated.""" prefork = "4" def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(RedisPyzorTest)) test_suite.addTest(unittest.makeSuite(ThreadsRedisPyzorTest)) test_suite.addTest(unittest.makeSuite(MaxThreadsRedisPyzorTest)) test_suite.addTest(unittest.makeSuite(PreForkRedisPyzorTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/functional/test_forwarder.py000066400000000000000000000076421244204744500232360ustar00rootroot00000000000000import os import time import shutil import unittest import subprocess import redis class ForwardSetup(object): """Setup forwarding client and 'remote' pyzord""" def write_homedir_file(self, name, content): if not name or not content: return with open(os.path.join(self.homedir, name), "w") as f: f.write(content) def __init__(self, homedir): self.homedir = homedir try: os.mkdir(homedir) except OSError: pass @unittest.skip("This fails randomly on PyPy.") class ForwarderTest(unittest.TestCase): def setUp(self): self.localserver = ForwardSetup('./pyzor-test-forwardserver') # we also use this dir for the local client self.localserver.write_homedir_file('servers', '127.0.0.1:9999\n') self.fwdclient = ForwardSetup('./pyzor-test-forwardingclient') self.fwdclient.write_homedir_file('servers', '127.0.0.1:9998\n') args = ["pyzord", "--homedir", self.localserver.homedir, '-e', 'redis', '--dsn', 'localhost,,,10', '-a', '127.0.0.1', '-p', '9999', '--forward-client-homedir', self.fwdclient.homedir] self.local_pyzord_proc = subprocess.Popen(args) self.remoteserver = ForwardSetup('./pyzor-test-remoteserver') args = ["pyzord", "--homedir", self.remoteserver.homedir, '-e', 'redis', '--dsn', 'localhost,,,9', '-a', '127.0.0.1', '-p', '9998'] self.remote_pyzord_proc = subprocess.Popen(args) time.sleep(0.3) def test_forward_report(self): # submit hash to local server for i in range(10): self.check_pyzor("report", self.localserver.homedir) # make sure the local submission worked self.check_pyzor("check", self.localserver.homedir, counts=(10, 0)) # now use the forwarding client's config to check forwarded submission time.sleep(1) self.check_pyzor("check", self.fwdclient.homedir, counts=(10, 0)) # submit the hash to the remote system, the count should go up self.check_pyzor("report", self.fwdclient.homedir) self.check_pyzor("check", self.fwdclient.homedir, counts=(11, 0)) # switch back to our local server, the count should still be the old value self.check_pyzor("check", self.localserver.homedir, counts=(10, 0)) def tearDown(self): if self.remote_pyzord_proc is not None: self.remote_pyzord_proc.kill() if self.local_pyzord_proc is not None: self.local_pyzord_proc.kill() shutil.rmtree(self.localserver.homedir, True) shutil.rmtree(self.fwdclient.homedir, True) shutil.rmtree(self.remoteserver.homedir, True) redis.StrictRedis(db=9).flushdb() redis.StrictRedis(db=10).flushdb() def check_pyzor(self, cmd, homedir, counts=None, msg=None): """simplified check_pyzor version from PyzorTestBase""" msg = "This is a test message for the forwading feature" args = ["pyzor", '--homedir', homedir, cmd] pyzor = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = pyzor.communicate(msg.encode("utf8")) if stderr: self.fail(stderr) try: stdout = stdout.decode("utf8") results = stdout.strip().split("\t") status = eval(results[1]) except Exception as e: self.fail("Parsing error: %s of %r" % (e, stdout)) self.assertEqual(status[0], 200, status) if counts: self.assertEqual(counts, (int(results[2]), int(results[3]))) return stdout def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(ForwarderTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/functional/test_pyzor.py000066400000000000000000000243531244204744500224240ustar00rootroot00000000000000import sys import redis import unittest from tests.util import * class PyzorScriptTest(PyzorTestBase): password_file = None access = """ALL : anonymous : allow """ def test_report_threshold(self): input = "Test1 report threshold 1 Test2" self.client_args["-r"] = "2" self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(1, 0)) self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(2, 0)) # Exit code will be success now, since the report count exceeds the # threshold self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(3, 0)) def test_whitelist_threshold(self): input = "Test1 white list threshold 1 Test2" self.client_args["-w"] = "2" self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(1, 0)) self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(1, 1)) self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(1, 2)) # Exit code will be failure now, since the whitelist count exceeds the # threshold self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(1, 3)) def test_report_whitelist_threshold(self): input = "Test1 report white list threshold 1 Test2" self.client_args["-w"] = "2" self.client_args["-r"] = "1" self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(1, 0)) # Exit code will be success now, since the report count exceeds the # thresholdRedisPyzorTest self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(2, 0)) self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(2, 1)) self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(2, 2)) # Exit code will be failure now, since the whitelist count exceeds the # threshold self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(2, 3)) def test_digest_style(self): input = "da39a3ee5e6b4b0d3255bfef95601890afd80700" self.client_args["-s"] = "digests" self.check_pyzor("pong", None, input=input, code=200, exit_code=0, counts=(sys.maxint, 0)) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(0, 0)) self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(1, 0)) self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(1, 1)) r = self.get_record(input, None) self.assertEqual(r["Count"], "1") self.assertEqual(r["WL-Count"], "1") def test_digest_style_multiple(self): input2 = "da39a3ee5e6b4b0d3255bfef95601890afd80705\n"\ "da39a3ee5e6b4b0d3255bfef95601890afd80706\n" input3 = "da39a3ee5e6b4b0d3255bfef95601890afd80705\n"\ "da39a3ee5e6b4b0d3255bfef95601890afd80706\n"\ "da39a3ee5e6b4b0d3255bfef95601890afd80707\n" self.client_args["-s"] = "digests" self.check_pyzor_multiple("pong", None, input=input3, exit_code=0, code=[200, 200, 200], counts=[(sys.maxint, 0), (sys.maxint, 0), (sys.maxint, 0)]) self.check_pyzor_multiple("check", None, input=input3, exit_code=1, code=[200, 200, 200], counts=[(0, 0), (0, 0), (0, 0)]) self.check_pyzor_multiple("report", None, input=input2, exit_code=0) self.check_pyzor_multiple("check", None, input=input3, exit_code=0, code=[200, 200, 200], counts=[(1, 0), (1, 0), (0, 0)]) self.check_pyzor_multiple("whitelist", None, input=input3, exit_code=0) self.check_pyzor_multiple("check", None, input=input3, exit_code=1, code=[200, 200, 200], counts=[(1, 1), (1, 1), (0, 1)]) def test_mbox_style(self): input = "From MAILER-DAEMON Mon Jan 6 15:13:33 2014\n\nTest1 message 0 Test2\n\n" self.client_args["-s"] = "mbox" self.check_pyzor("pong", None, input=input, code=200, exit_code=0, counts=(sys.maxint, 0)) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(0, 0)) self.check_pyzor("report", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=0, counts=(1, 0)) self.check_pyzor("whitelist", None, input=input, code=200, exit_code=0) self.check_pyzor("check", None, input=input, code=200, exit_code=1, counts=(1, 1)) r = self.get_record(input, None) self.assertEqual(r["Count"], "1") self.assertEqual(r["WL-Count"], "1") def test_mbox_style_multiple(self): input2 = "From MAILER-DAEMON Mon Jan 6 15:08:02 2014\n\nTest1 message 1 Test2\n\n"\ "From MAILER-DAEMON Mon Jan 6 15:08:05 2014\n\nTest1 message 2 Test2\n\n" input3 = "From MAILER-DAEMON Mon Jan 6 15:08:02 2014\n\nTest1 message 1 Test2\n\n"\ "From MAILER-DAEMON Mon Jan 6 15:08:05 2014\n\nTest1 message 2 Test2\n\n"\ "From MAILER-DAEMON Mon Jan 6 15:08:08 2014\n\nTest1 message 3 Test2\n\n" self.client_args["-s"] = "mbox" self.check_pyzor_multiple("pong", None, input=input3, exit_code=0, code=[200, 200, 200], counts=[(sys.maxint, 0), (sys.maxint, 0), (sys.maxint, 0)]) self.check_pyzor_multiple("check", None, input=input3, exit_code=1, code=[200, 200, 200], counts=[(0, 0), (0, 0), (0, 0)]) self.check_pyzor_multiple("report", None, input=input2, exit_code=0) self.check_pyzor_multiple("check", None, input=input3, exit_code=0, code=[200, 200, 200], counts=[(1, 0), (1, 0), (0, 0)]) self.check_pyzor_multiple("whitelist", None, input=input3, exit_code=0) self.check_pyzor_multiple("check", None, input=input3, exit_code=1, code=[200, 200, 200], counts=[(1, 1), (1, 1), (0, 1)]) def test_predigest(self): out = self.check_pyzor("predigest", None, input=msg).strip() self.assertEqual(out.decode("utf8"), "TestEmail") def test_digest(self): out = self.check_pyzor("digest", None, input=msg).strip() self.assertEqual(out.decode("utf8"), digest) class MultipleServerPyzorScriptTest(PyzorTestBase): password_file = None access = """ALL : anonymous : allow """ servers = """127.0.0.1:9999 127.0.0.1:9998 127.0.0.1:9997 """ def test_ping(self): self.check_pyzor_multiple("ping", None, exit_code=0, code=[200, 200, 200]) def test_pong(self): input = "Test1 multiple pong Test2" self.check_pyzor_multiple("pong", None, input=input, exit_code=0, code=[200, 200, 200], counts=[(sys.maxint, 0), (sys.maxint, 0), (sys.maxint, 0)]) def test_check(self): input = "Test1 multiple check Test2" self.check_pyzor_multiple("check", None, input=input, exit_code=1, code=[200, 200, 200], counts=[(0, 0), (0, 0), (0, 0)]) def test_report(self): input = "Test1 multiple report Test2" self.check_pyzor_multiple("report", None, input=input, exit_code=0, code=[200, 200, 200]) def test_whitelist(self): input = "Test1 multiple whitelist Test2" self.check_pyzor_multiple("whitelist", None, input=input, exit_code=0, code=[200, 200, 200]) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(PyzorScriptTest)) test_suite.addTest(unittest.makeSuite(MultipleServerPyzorScriptTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/functional/test_server.py000066400000000000000000000171501244204744500225440ustar00rootroot00000000000000import sys import time import errno import unittest import ConfigParser import pyzor.client from tests.util import * try: import MySQLdb has_mysql = True except ImportError: has_mysql = False try: import redis has_redis = True except ImportError: has_redis = False try: import gdbm has_gdbm = True except ImportError: has_gdbm = False class BatchedDigestsTest(object): def setUp(self): PyzorTestBase.setUp(self) self.client = pyzor.client.BatchClient() def test_batched_report(self): digest = "da39a3ee5e6b4b0d3255bfef95601890afd80709" for i in range(9): self.client.report(digest, ("127.0.0.1", 9999)) self.check_digest(digest, ("127.0.0.1", 9999)) self.client.report(digest, ("127.0.0.1", 9999)) self.check_digest(digest, ("127.0.0.1", 9999), (10, 0)) def test_batched_whitelist(self): digest = "da39a3ee5e6b4b0d3255bfef95601890afd80708" for i in range(9): self.client.whitelist(digest, ("127.0.0.1", 9999)) self.check_digest(digest, ("127.0.0.1", 9999)) self.client.whitelist(digest, ("127.0.0.1", 9999)) self.check_digest(digest, ("127.0.0.1", 9999), (0, 10)) def test_batched_combined(self): digest = "da39a3ee5e6b4b0d3255bfef95601890afd80707" for i in range(9): self.client.report(digest, ("127.0.0.1", 9999)) self.client.whitelist(digest, ("127.0.0.1", 9999)) self.check_digest(digest, ("127.0.0.1", 9999)) self.client.report(digest, ("127.0.0.1", 9999)) self.check_digest(digest, ("127.0.0.1", 9999), (10, 0)) self.client.whitelist(digest, ("127.0.0.1", 9999)) self.check_digest(digest, ("127.0.0.1", 9999), (10, 10)) def test_batched_multiple_report(self): digest = "%sa39a3ee5e6b4b0d3255bfef95601890afd80706" for i in range(10): self.client.report(digest % i, ("127.0.0.1", 9999)) for i in range(10): self.check_digest(digest % i, ("127.0.0.1", 9999), (1, 0)) def test_batched_multiple_whitelist(self): digest = "%sa39a3ee5e6b4b0d3255bfef95601890afd80705" for i in range(10): self.client.whitelist(digest % i, ("127.0.0.1", 9999)) for i in range(10): self.check_digest(digest % i, ("127.0.0.1", 9999), (0, 1)) def test_multiple_addresses_report(self): digest1 = "da39a3ee5e6b4b0d3255bfef95601890afd80704" digest2 = "da39a3ee5e6b4b0d3255bfef95601890afd80703" for i in range(9): self.client.report(digest1, ("127.0.0.1", 9999)) self.client.report(digest2, ("127.0.0.1", 9998)) self.check_digest(digest1, ("127.0.0.1", 9999)) self.check_digest(digest2, ("127.0.0.1", 9998)) self.client.report(digest1, ("127.0.0.1", 9999)) self.check_digest(digest1, ("127.0.0.1", 9999), (10, 0)) self.client.report(digest2, ("127.0.0.1", 9998)) self.check_digest(digest2, ("127.0.0.1", 9998), (10, 0)) def test_multiple_addresses_whitelist(self): digest1 = "da39a3ee5e6b4b0d3255bfef95601890afd80702" digest2 = "da39a3ee5e6b4b0d3255bfef95601890afd80701" for i in range(9): self.client.whitelist(digest1, ("127.0.0.1", 9999)) self.client.whitelist(digest2, ("127.0.0.1", 9998)) self.check_digest(digest1, ("127.0.0.1", 9999)) self.check_digest(digest2, ("127.0.0.1", 9998)) self.client.whitelist(digest1, ("127.0.0.1", 9999)) self.check_digest(digest1, ("127.0.0.1", 9999), (0, 10)) self.client.whitelist(digest2, ("127.0.0.1", 9998)) self.check_digest(digest2, ("127.0.0.1", 9998), (0, 10)) schema = """ CREATE TABLE IF NOT EXISTS `%s` ( `digest` char(40) default NULL, `r_count` int(11) default NULL, `wl_count` int(11) default NULL, `r_entered` datetime default NULL, `wl_entered` datetime default NULL, `r_updated` datetime default NULL, `wl_updated` datetime default NULL, PRIMARY KEY (`digest`) ) """ @unittest.skipIf(not os.path.exists("./test.conf"), "test.conf is not available") @unittest.skipIf(not has_mysql, "MySQLdb library not available") class MySQLdbBatchedPyzorTest(BatchedDigestsTest, PyzorTestBase): """Test the mysql engine.""" dsn = None engine = "mysql" password_file = None access = """ALL : anonymous : allow """ servers = """127.0.0.1:9999 127.0.0.1:9998 """ @classmethod def setUpClass(cls): conf = ConfigParser.ConfigParser() conf.read("./test.conf") table = conf.get("test", "table") db = MySQLdb.Connect(host=conf.get("test", "host"), user=conf.get("test", "user"), passwd=conf.get("test", "passwd"), db=conf.get("test", "db")) c = db.cursor() c.execute(schema % table) c.close() db.close() cls.dsn = "%s,%s,%s,%s,%s" % (conf.get("test", "host"), conf.get("test", "user"), conf.get("test", "passwd"), conf.get("test", "db"), conf.get("test", "table")) super(MySQLdbBatchedPyzorTest, cls).setUpClass() @classmethod def tearDownClass(cls): super(MySQLdbBatchedPyzorTest, cls).tearDownClass() try: conf = ConfigParser.ConfigParser() conf.read("./test.conf") table = conf.get("test", "table") db = MySQLdb.Connect(host=conf.get("test", "host"), user=conf.get("test", "user"), passwd=conf.get("test", "passwd"), db=conf.get("test", "db")) c = db.cursor() c.execute("DROP TABLE %s" % table) c.close() db.close() except: pass @unittest.skipIf(not has_redis, "redis library not available") class RedisBatchedPyzorTest(BatchedDigestsTest, PyzorTestBase): """Test the redis engine""" dsn = "localhost,,,10" engine = "redis" password_file = None access = """ALL : anonymous : allow """ servers = """127.0.0.1:9999 127.0.0.1:9998 """ @classmethod def tearDownClass(cls): super(RedisBatchedPyzorTest, cls).tearDownClass() redis.StrictRedis(db=10).flushdb() class DetachPyzorTest(PyzorTestBase): detach = "/dev/null" homedir = os.path.join(os.getcwd(), "pyzor-test") def test_pid(self): self.assertTrue(os.path.exists(os.path.join(self.homedir, "pyzord.pid"))) @staticmethod def is_running(pid): try: os.kill(pid, 0) except OSError as err: if err.errno == errno.ESRCH: return False return True @classmethod def tearDownClass(cls): with open(os.path.join(cls.homedir, "pyzord.pid")) as pidf: pid = int(pidf.read().strip()) os.kill(pid, 15) while cls.is_running(pid): time.sleep(0.25) super(DetachPyzorTest, cls).tearDownClass() def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(MySQLdbBatchedPyzorTest)) # test_suite.addTest(unittest.makeSuite(GdbmBatchedPyzorTest)) test_suite.addTest(unittest.makeSuite(RedisBatchedPyzorTest)) test_suite.addTest(unittest.makeSuite(DetachPyzorTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/000077500000000000000000000000001244204744500164365ustar00rootroot00000000000000pyzor-release-1-0-0/tests/unit/__init__.py000066400000000000000000000016011244204744500205450ustar00rootroot00000000000000"""A suite of unit tests that verifies the correct behaviour of various functions/methods in the pyzord code. Note these tests the source of pyzor, not the version currently installed. """ import unittest def suite(): """Gather all the tests from this package in a test suite.""" import test_client import test_config import test_digest import test_server import test_account import test_forwarder import test_engines test_suite = unittest.TestSuite() test_suite.addTest(test_engines.suite()) test_suite.addTest(test_client.suite()) test_suite.addTest(test_config.suite()) test_suite.addTest(test_digest.suite()) test_suite.addTest(test_server.suite()) test_suite.addTest(test_account.suite()) test_suite.addTest(test_forwarder.suite()) return test_suite if __name__ == '__main__': unittest.main(defaultTest='suite') pyzor-release-1-0-0/tests/unit/test_account.py000066400000000000000000000201361244204744500215050ustar00rootroot00000000000000"""Test the pyzor.account module """ import os import sys import time import email import hashlib import unittest import StringIO import pyzor import pyzor.config import pyzor.account class AccountTest(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.timestamp = 1381219396 self.msg = email.message_from_string("") self.msg["Op"] = "ping" self.msg["Thread"] = "14941" self.msg["PV"] = "2.1" self.msg["User"] = "anonymous" self.msg["Time"] = str(self.timestamp) def tearDown(self): unittest.TestCase.tearDown(self) def test_sign_msg(self): """Test the sign message function""" hashed_key = hashlib.sha1(b"test_key").hexdigest() expected = "2ab1bad2aae6fd80c656a896c82eef0ec1ec38a0" result = pyzor.account.sign_msg(hashed_key, self.timestamp, self.msg) self.assertEqual(result, expected) def test_hash_key(self): """Test the hash key function""" user = "testuser" key = "testkey" expected = "0957bd79b58263657127a39762879098286d8477" result = pyzor.account.hash_key(key, user) self.assertEqual(result, expected) def test_verify_signature(self): """Test the verify signature function""" def mock_sm(h, t, m): return "testsig" real_sm = pyzor.account.sign_msg pyzor.account.sign_msg = mock_sm try: self.msg["Sig"] = "testsig" del self.msg["Time"] self.msg["Time"] = str(int(time.time())) pyzor.account.verify_signature(self.msg, "testkey") finally: pyzor.account.sign_msg = real_sm def test_verify_signature_old_timestamp(self): """Test the verify signature with old timestamp""" def mock_sm(h, t, m): return "testsig" real_sm = pyzor.account.sign_msg pyzor.account.sign_msg = mock_sm try: self.msg["Sig"] = "testsig" self.assertRaises(pyzor.SignatureError, pyzor.account.verify_signature, self.msg, "testkey") finally: pyzor.account.sign_msg = real_sm def test_verify_signature_bad_signature(self): """Test the verify signature with invalid signature""" def mock_sm(h, t, m): return "testsig" real_sm = pyzor.account.sign_msg pyzor.account.sign_msg = mock_sm try: self.msg["Sig"] = "testsig-bad" del self.msg["Time"] self.msg["Time"] = str(int(time.time())) self.assertRaises(pyzor.SignatureError, pyzor.account.verify_signature, self.msg, "testkey") finally: pyzor.account.sign_msg = real_sm class LoadAccountTest(unittest.TestCase): """Tests for the load_accounts function""" filepath = "test_file" def setUp(self): unittest.TestCase.setUp(self) self.real_exists = os.path.exists os.path.exists = lambda p: True if p == self.filepath else \ self.real_exists(p) self.mock_file = StringIO.StringIO() try: self.real_open = pyzor.account.__builtins__.open except AttributeError: self.real_open = pyzor.account.__builtins__["open"] def mock_open(path, mode="r", buffering=-1): if path == self.filepath: self.mock_file.seek(0) return self.mock_file else: return self.real_open(path, mode, buffering) try: pyzor.account.__builtins__.open = mock_open except AttributeError: pyzor.account.__builtins__["open"] = mock_open def tearDown(self): unittest.TestCase.tearDown(self) os.path.exists = self.real_exists try: pyzor.account.__builtins__.open = self.real_open except AttributeError: pyzor.account.__builtins__["open"] = self.real_open def test_load_accounts_nothing(self): result = pyzor.config.load_accounts("foobar") self.assertEqual(result, {}) def test_load_accounts(self): """Test loading the account file""" self.mock_file.write("public.pyzor.org : 24441 : test : 123abc,cba321\n" "public2.pyzor.org : 24441 : test2 : 123abc,cba321") result = pyzor.config.load_accounts(self.filepath) self.assertIn(("public.pyzor.org", 24441), result) self.assertIn(("public2.pyzor.org", 24441), result) account = result[("public.pyzor.org", 24441)] self.assertEqual((account.username, account.salt, account.key), ("test", "123abc", "cba321")) account = result[("public2.pyzor.org", 24441)] self.assertEqual((account.username, account.salt, account.key), ("test2", "123abc", "cba321")) def test_load_accounts_invalid_line(self): """Test loading the account file""" self.mock_file.write("public.pyzor.org : 24441 ; test : 123abc,cba321\n" "public2.pyzor.org : 24441 : test2 : 123abc,cba321") result = pyzor.config.load_accounts(self.filepath) self.assertNotIn(("public.pyzor.org", 24441), result) self.assertEquals(len(result), 1) self.assertIn(("public2.pyzor.org", 24441), result) account = result[("public2.pyzor.org", 24441)] self.assertEqual((account.username, account.salt, account.key), ("test2", "123abc", "cba321")) def test_load_accounts_invalid_port(self): """Test loading the account file""" self.mock_file.write("public.pyzor.org : a4441 : test : 123abc,cba321\n" "public2.pyzor.org : 24441 : test2 : 123abc,cba321") result = pyzor.config.load_accounts(self.filepath) self.assertNotIn(("public.pyzor.org", 24441), result) self.assertEquals(len(result), 1) self.assertIn(("public2.pyzor.org", 24441), result) account = result[("public2.pyzor.org", 24441)] self.assertEqual((account.username, account.salt, account.key), ("test2", "123abc", "cba321")) def test_load_accounts_invalid_key(self): """Test loading the account file""" self.mock_file.write("public.pyzor.org : 24441 : test : ,\n" "public2.pyzor.org : 24441 : test2 : 123abc,cba321") result = pyzor.config.load_accounts(self.filepath) self.assertNotIn(("public.pyzor.org", 24441), result) self.assertEquals(len(result), 1) self.assertIn(("public2.pyzor.org", 24441), result) account = result[("public2.pyzor.org", 24441)] self.assertEqual((account.username, account.salt, account.key), ("test2", "123abc", "cba321")) def test_load_accounts_invalid_missing_comma(self): """Test loading the account file""" self.mock_file.write("public.pyzor.org : 24441 : test : 123abccba321\n" "public2.pyzor.org : 24441 : test2 : 123abc,cba321") result = pyzor.config.load_accounts(self.filepath) self.assertNotIn(("public.pyzor.org", 24441), result) self.assertEquals(len(result), 1) self.assertIn(("public2.pyzor.org", 24441), result) account = result[("public2.pyzor.org", 24441)] self.assertEqual((account.username, account.salt, account.key), ("test2", "123abc", "cba321")) def test_load_accounts_comment(self): """Test skipping commented lines""" self.mock_file.write("#public1.pyzor.org : 24441 : test : 123abc,cba321") result = pyzor.config.load_accounts(self.filepath) self.assertNotIn(("public.pyzor.org", 24441), result) self.assertFalse(result) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(AccountTest)) test_suite.addTest(unittest.makeSuite(LoadAccountTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_client.py000066400000000000000000000275651244204744500213440ustar00rootroot00000000000000import time import email import unittest try: from unittest.mock import Mock, patch, call except ImportError: from mock import Mock, patch, call import pyzor.client import pyzor.account class TestBase(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.thread = 33715 self.timeout = None self.time = str(int(time.time())) patch("pyzor.account.sign_msg", return_value="TestSig").start() patch("pyzor.account.hash_key").start() # the response the mock socket will send self.response = {"Code": "200", "Diag": "OK", "PV": "2.1", "Thread": "33715", "Time": self.time } self.mresponse = None self.mock_socket = None # the expected request that the client should send self.expected = {"Thread": str(self.thread), "PV": str(pyzor.proto_version), "User": "anonymous", "Time": self.time, "Sig": "TestSig" } def tearDown(self): unittest.TestCase.tearDown(self) patch.stopall() def get_requests(self): for mock_call in self.mock_socket.mock_calls: name, args, kwargs = mock_call if name == "socket().sendto": yield args, kwargs def check_request(self): """Check if the request sent by the client is equal to the expected one. """ req = {} for args, _ in self.get_requests(): self.assertEqual(args[2], ('127.0.0.1', 24441)) req = dict(email.message_from_string(args[0].decode())) self.assertEqual(req, self.expected) def patch_all(self, conf=None): if conf is None: conf = {} patch("pyzor.message.ThreadId.generate", return_value=self.thread).start() if self.response: response = "\n".join("%s: %s" % (key, value) for key, value in self.response.items()) + "\n\n" self.mresponse = response.encode(), ("127.0.0.1", 24441) else: self.mresponse = None addrinfo = [(2, 2, 17, '', ('127.0.0.1', 24441))] config = {"socket.return_value": Mock(), "socket.return_value.recvfrom.return_value": self.mresponse, "getaddrinfo.return_value": addrinfo} config.update(conf) self.mock_socket = patch("pyzor.client.socket", **config).start() def check_client(self, accounts, method, *args, **kwargs): """Tests if the request and response are sent and read correctly by the client. """ client = pyzor.client.Client(accounts=accounts, timeout=self.timeout) got_response = getattr(client, method)(*args, **kwargs) self.assertEqual(str(got_response), self.mresponse[0].decode()) if self.expected is not None: self.check_request() return client class ClientTest(TestBase): def test_ping(self): """Test the client ping request""" self.expected["Op"] = "ping" self.patch_all() self.check_client(None, "ping") def test_pong(self): """Test the client pong request""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.expected["Op"] = "pong" self.expected["Op-Digest"] = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() self.check_client(None, "pong", digest) def test_check(self): """Test the client check request""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.expected["Op"] = "check" self.expected["Op-Digest"] = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() self.check_client(None, "check", digest) def test_info(self): """Test the client info request""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.expected["Op"] = "info" self.expected["Op-Digest"] = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() self.check_client(None, "info", digest) def test_report(self): """Test the client report request""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.expected["Op"] = "report" self.expected["Op-Digest"] = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.expected["Op-Spec"] = "20,3,60,3" self.patch_all() self.check_client(None, "report", digest) def test_whitelist(self): """Test the client whitelist request""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.expected["Op"] = "whitelist" self.expected["Op-Digest"] = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.expected["Op-Spec"] = "20,3,60,3" self.patch_all() self.check_client(None, "whitelist", digest) def test_handle_account(self): """Test client handling accounts""" test_account = pyzor.account.Account("TestUser", "TestKey", "TestSalt") self.expected["Op"] = "ping" self.expected["User"] = "TestUser" self.patch_all() self.check_client({("public.pyzor.org", 24441): test_account}, "ping") def test_handle_invalid_thread(self): """Test invalid thread id""" self.thread += 20 self.expected["Op"] = "ping" self.patch_all() self.assertRaises(pyzor.ProtocolError, self.check_client, None, "ping") def test_set_timeout(self): self.expected = None self.patch_all() self.timeout = 10 self.check_client(None, "ping") calls = [call.socket().settimeout(10), ] self.mock_socket.assert_has_calls(calls) class BatchClientTest(TestBase): def test_report(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(10): client.report(digest) args, kwargs = list(self.get_requests())[0] msg = email.message_from_string(args[0].decode()) self.assertEqual(len(msg.get_all("Op-Digest")), 10) def test_report_to_few(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(9): client.report(digest) self.assertEqual(list(self.get_requests()), []) def test_whitelist(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(10): client.whitelist(digest) args, kwargs = list(self.get_requests())[0] msg = email.message_from_string(args[0].decode()) self.assertEqual(len(msg.get_all("Op-Digest")), 10) def test_whitelist_to_few(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(9): client.whitelist(digest) self.assertEqual(list(self.get_requests()), []) def test_force_report(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(9): client.report(digest) client.force() args, kwargs = list(self.get_requests())[0] msg = email.message_from_string(args[0].decode()) self.assertEqual(len(msg.get_all("Op-Digest")), 9) def test_force_whitelist(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(9): client.whitelist(digest) client.force() args, kwargs = list(self.get_requests())[0] msg = email.message_from_string(args[0].decode()) self.assertEqual(len(msg.get_all("Op-Digest")), 9) def test_flush_report(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(9): client.report(digest) client.flush() client.report(digest) self.assertEqual(list(self.get_requests()), []) def test_flush_whitelist(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.patch_all() client = pyzor.client.BatchClient() for i in range(9): client.whitelist(digest) client.flush() client.whitelist(digest) self.assertEqual(list(self.get_requests()), []) class ClientRunnerTest(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.server = "test.example.com", 24441 def tearDown(self): unittest.TestCase.tearDown(self) def check_runner(self, test_class, response, results, kwargs=None): if kwargs is None: kwargs = {} kwargs["return_value"] = response mock_routine = Mock(**kwargs) runner = test_class(mock_routine) runner.run(self.server, ()) self.assertEqual(runner.results, results) return runner def test_normal(self): response = pyzor.message.Response() response["Diag"] = "OK" response["Code"] = "200" server = "%s:%s\t" % self.server results = ["%s%s\n" % (server, response.head_tuple()), ] self.check_runner(pyzor.client.ClientRunner, response, results) def test_check(self): response = pyzor.message.Response() response["Diag"] = "OK" response["Code"] = "200" response["Count"] = "2" response["WL-Count"] = "1" server = "%s:%s\t" % self.server results = ["%s%s\t%s\t%s\n" % (server, response.head_tuple(), "2", "1")] self.check_runner(pyzor.client.CheckClientRunner, response, results) def test_info(self): response = """Code: 200 Diag: OK PV: 2.1 Thread: 8521 Entered: 1400221786 Updated: 1400221794 WL-Entered: 0 WL-Updated: 0 Count: 4 WL-Count: 0 """ response = email.message_from_string(response, _class=pyzor.message.Response) server = "%s:%s" % self.server result = ("%s\t(200, 'OK')\n" "\tCount: 4\n" "\tEntered: %s\n" "\tUpdated: %s\n" "\tWL-Count: 0\n" "\tWL-Entered: %s\n" "\tWL-Updated: %s\n\n" % (server, time.ctime(1400221786), time.ctime(1400221794), time.ctime(0), time.ctime(0))) self.maxDiff = None self.check_runner(pyzor.client.InfoClientRunner, response, [result]) def test_info_never(self): response = """Code: 200 Diag: OK PV: 2.1 Thread: 8521 Entered: 1400221786 Updated: 1400221794 WL-Entered: -1 WL-Updated: -1 Count: 4 WL-Count: 0 """ response = email.message_from_string(response, _class=pyzor.message.Response) server = "%s:%s" % self.server result = ("%s\t(200, 'OK')\n" "\tCount: 4\n" "\tEntered: %s\n" "\tUpdated: %s\n" "\tWL-Count: 0\n" "\tWL-Entered: Never\n" "\tWL-Updated: Never\n\n" % (server, time.ctime(1400221786), time.ctime(1400221794))) self.maxDiff = None self.check_runner(pyzor.client.InfoClientRunner, response, [result]) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(ClientTest)) test_suite.addTest(unittest.makeSuite(BatchClientTest)) test_suite.addTest(unittest.makeSuite(ClientRunnerTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_config.py000066400000000000000000000231561244204744500213230ustar00rootroot00000000000000import os import logging import unittest import ConfigParser try: from unittest.mock import patch, Mock except ImportError: from mock import patch, Mock import pyzor.config from tests.util import mock_open class MockData(list): def close(self): pass class TestPasswdLoad(unittest.TestCase): fp = "pyzord.passwd" alice_key = "alice_key" bob_key = "bob_key" def setUp(self): super(TestPasswdLoad, self).setUp() self.data = MockData() self.exists = True real_exists = os.path.exists patch("pyzor.config.open", return_value=self.data, create=True).start() _exists = lambda fp: True if fp == self.fp else real_exists(fp) patch("pyzor.config.os.path.exists", side_effect=_exists).start() def get_passwd(self, fp=None): if not fp: fp = self.fp return pyzor.config.load_passwd_file(fp) def tearDown(self): super(TestPasswdLoad, self).tearDown() patch.stopall() def test_nothing(self): result = self.get_passwd() self.assertEqual(result, {}) def test_default(self): result = self.get_passwd("foobar") self.assertEqual(result, {}) def test_passwd(self): self.data.append("alice : %s\n" % self.alice_key) self.data.append("bob : %s\n" % self.bob_key) result = self.get_passwd() self.assertEqual(result, {"alice": self.alice_key, "bob": self.bob_key}) def test_invalid_line(self): self.data.append("alice ; %s\n" % self.alice_key) self.data.append("bob : %s\n" % self.bob_key) result = self.get_passwd() self.assertEqual(result, {"bob": self.bob_key}) def test_ignore_comment(self): self.data.append("alice : %s\n" % self.alice_key) self.data.append("# bob : %s\n" % self.bob_key) result = self.get_passwd() self.assertEqual(result, {"alice": self.alice_key}) class TestAccessLoad(unittest.TestCase): fp = "pyzord.access" accounts = ["alice", "bob"] all = {'report', 'info', 'pong', 'ping', 'check', 'whitelist'} anonymous_privileges = {'report', 'info', 'pong', 'ping', 'check'} def setUp(self): super(TestAccessLoad, self).setUp() self.data = MockData() self.exists = True real_exists = os.path.exists patch("pyzor.config.open", return_value=self.data, create=True).start() _exists = lambda fp: True if fp == self.fp else real_exists(fp) patch("pyzor.config.os.path.exists", side_effect=_exists).start() def get_access(self, fp=None, accounts=None): if not fp: fp = self.fp if not accounts: accounts = self.accounts return pyzor.config.load_access_file(fp, accounts) def tearDown(self): super(TestAccessLoad, self).tearDown() patch.stopall() def test_nothing(self): result = self.get_access() self.assertEqual(result, {}) def test_default(self): result = self.get_access(fp="foobar") self.assertEqual(result, {'anonymous': self.anonymous_privileges}) def test_invalid_line(self): self.data.append("all : allice ; allow\n") self.data.append("ping : bob : allow\n") result = self.get_access() self.assertEqual(result, {'bob': {'ping'}}) def test_invalid_action(self): self.data.append("all : allice : don't allow\n") self.data.append("ping : bob : allow\n") result = self.get_access() self.assertEqual(result, {'bob': {'ping'}}) def test_all_privilege(self): self.data.append("all : bob : allow\n") result = self.get_access() self.assertEqual(result, {'bob': self.all}) def test_all_accounts(self): self.data.append("all : all : allow\n") result = self.get_access() self.assertEqual(result, {'alice': self.all, 'bob': self.all}) def test_deny_action(self): self.data.append("all : all : allow\n") self.data.append("ping : bob : deny\n") result = self.get_access() self.assertEqual(result, {'alice': self.all, 'bob': self.all - {'ping'}}) def test_multiple_users(self): self.data.append("all : alice bob: allow\n") result = self.get_access() self.assertEqual(result, {'alice': self.all, 'bob': self.all}) def test_multiple_privileges(self): self.data.append("ping pong : alice: allow\n") result = self.get_access() self.assertEqual(result, {'alice': {'ping', 'pong'}}) def test_ignore_comments(self): self.data.append("all: alice: allow\n") self.data.append("# all: bob : allow\n") result = self.get_access() self.assertEqual(result, {'alice': self.all}) class TestServersLoad(unittest.TestCase): fp = "servers" public_server = ("public.pyzor.org", 24441) random_server1 = ("random.pyzor.org", 33544) random_server2 = ("127.1.2.45", 13587) def setUp(self): super(TestServersLoad, self).setUp() self.data = [] self.exists = True real_exists = os.path.exists _exists = lambda fp: True if fp == self.fp else real_exists(fp) patch("pyzor.config.os.path.exists", side_effect=_exists).start() def get_servers(self, fp=None): if not fp: fp = self.fp name = "pyzor.config.open" with patch(name, mock_open(read_data=''.join(self.data)), create=True) as m: return pyzor.config.load_servers(fp) def tearDown(self): super(TestServersLoad, self).tearDown() patch.stopall() def test_nothing(self): result = self.get_servers() self.assertEqual(result, [self.public_server]) def test_default(self): result = self.get_servers("foobar") self.assertEqual(result, [self.public_server]) def test_servers(self): self.data.append("%s:%s\n" % self.random_server1) self.data.append("%s:%s\n" % self.random_server2) result = self.get_servers() self.assertEqual(result, [self.random_server1, self.random_server2]) def test_ignore_comment(self): self.data.append("#%s:%s\n" % self.random_server1) self.data.append("%s:%s\n" % self.random_server2) result = self.get_servers() self.assertEqual(result, [self.random_server2]) class TestLogSetup(unittest.TestCase): log_file = "this_is_a_test_log_file" def setUp(self): super(TestLogSetup, self).setUp() def tearDown(self): super(TestLogSetup, self).tearDown() try: os.remove(self.log_file) except OSError: pass def test_logging(self): pyzor.config.setup_logging("pyzor.test1", None, False) log = logging.getLogger("pyzor.test1") self.assertEqual(log.getEffectiveLevel(), logging.INFO) self.assertEqual(log.handlers[0].level, logging.CRITICAL) def test_logging_debug(self): pyzor.config.setup_logging("pyzor.test2", None, True) log = logging.getLogger("pyzor.test2") self.assertEqual(log.getEffectiveLevel(), logging.DEBUG) self.assertEqual(log.handlers[0].level, logging.DEBUG) def test_logging_file(self): pyzor.config.setup_logging("pyzor.test3", self.log_file, False) log = logging.getLogger("pyzor.test3") self.assertEqual(log.getEffectiveLevel(), logging.INFO) self.assertEqual(log.handlers[0].level, logging.CRITICAL) self.assertEqual(log.handlers[1].level, logging.INFO) def test_logging_file_debug(self): pyzor.config.setup_logging("pyzor.test4", self.log_file, True) log = logging.getLogger("pyzor.test4") self.assertEqual(log.getEffectiveLevel(), logging.DEBUG) self.assertEqual(log.handlers[0].level, logging.DEBUG) self.assertEqual(log.handlers[1].level, logging.DEBUG) class TestExpandHomeFiles(unittest.TestCase): home = "/home/user/pyzor" def setUp(self): super(TestExpandHomeFiles, self).setUp() def tearDown(self): super(TestExpandHomeFiles, self).tearDown() def check_expand(self, homefiles, homedir, config, expected): section = "test" conf = ConfigParser.ConfigParser() conf.add_section(section) for key, value in config.iteritems(): conf.set(section, key, value) pyzor.config.expand_homefiles(homefiles, section, homedir, conf) result = dict(conf.items(section)) self.assertEqual(result, expected) def test_homedir(self): self.check_expand( ["testfile"], self.home, {"testfile": "my.file"}, {"testfile": "%s/my.file" % self.home}, ) def test_homedir_none(self): self.check_expand( ["testfile"], self.home, {"testfile": ""}, {"testfile": ""}, ) def test_homedir_abs(self): self.check_expand( ["testfile"], self.home, {"testfile": "/home/user2/pyzor"}, {"testfile": "/home/user2/pyzor"}, ) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(TestLogSetup)) test_suite.addTest(unittest.makeSuite(TestAccessLoad)) test_suite.addTest(unittest.makeSuite(TestPasswdLoad)) test_suite.addTest(unittest.makeSuite(TestServersLoad)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_digest.py000066400000000000000000000251641244204744500213360ustar00rootroot00000000000000"""The the pyzor.digest module """ import unittest import pyzor.digest from pyzor.digest import * try: from unittest.mock import patch, Mock, call except ImportError: from mock import patch, Mock, call HTML_TEXT = """Email spam

Email spam, also known as junk email or unsolicited bulk email (UBE), is a subset of electronic spam involving nearly identical messages sent to numerous recipients by email. Clicking on links in spam email may send users to phishing web sites or sites that are hosting malware.""" HTML_TEXT_STRIPED = 'Email spam Email spam , also known as junk email or unsolicited bulk email ( UBE ),' \ ' is a subset of electronic spam involving nearly identical messages sent to numerous recipients by email' \ ' . Clicking on links in spam email may send users to phishing web sites or sites that are hosting malware .' HTML_STYLE = """ This is a test. """ HTML_SCRIPT = """ This is a test. """ class HTMLStripperTests(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.data = [] def tearDown(self): unittest.TestCase.tearDown(self) def test_HTMLStripper(self): stripper = HTMLStripper(self.data) stripper.feed(HTML_TEXT) res = " ".join(self.data) self.assertEqual(res, HTML_TEXT_STRIPED) def test_strip_style(self): stripper = HTMLStripper(self.data) stripper.feed(HTML_STYLE) res = " ".join(self.data) self.assertEqual(res, "This is a test.") def test_strip_script(self): stripper = HTMLStripper(self.data) stripper.feed(HTML_SCRIPT) res = " ".join(self.data) self.assertEqual(res, "This is a test.") class PreDigestTests(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.lines = [] def mock_digest_paylods(c, message): yield message.decode("utf8") def mock_handle_line(s, line): self.lines.append(line.decode("utf8")) self.real_digest_payloads = DataDigester.digest_payloads self.real_handle_line = DataDigester.handle_line DataDigester.digest_payloads = mock_digest_paylods DataDigester.handle_line = mock_handle_line def tearDown(self): unittest.TestCase.tearDown(self) DataDigester.digest_payloads = self.real_digest_payloads DataDigester.handle_line = self.real_handle_line def test_predigest_emails(self): """Test email removal in the predigest process""" real_longstr = DataDigester.longstr_ptrn DataDigester.longstr_ptrn = re.compile(r'\S{100,}') emails = ["test@example.com", "test123@example.com", "test+abc@example.com", "test.test2@example.com", "test.test2+abc@example.com", ] message = "Test %s Test2" expected = "TestTest2" try: for email in emails: self.lines = [] DataDigester((message % email).encode("utf8")) self.assertEqual(self.lines[0], expected) finally: DataDigester.longstr_ptrn = real_longstr def test_predigest_urls(self): """Test url removal in the predigest process""" real_longstr = DataDigester.longstr_ptrn DataDigester.longstr_ptrn = re.compile(r'\S{100,}') urls = ["http://www.example.com", # "www.example.com", # XXX This also fail "http://example.com", # "example.com", # XXX This also fails "http://www.example.com/test/" "http://www.example.com/test/test2", ] message = "Test %s Test2" expected = "TestTest2" try: for url in urls: self.lines = [] DataDigester((message % url).encode("utf8")) self.assertEqual(self.lines[0], expected) finally: DataDigester.longstr_ptrn = real_longstr def test_predigest_long(self): """Test long "words" removal in the predigest process""" strings = ["0A2D3f%a#S", "3sddkf9jdkd9", "@@#@@@@@@@@@"] message = "Test %s Test2" expected = "TestTest2" for string in strings: self.lines = [] DataDigester((message % string).encode("utf8")) self.assertEqual(self.lines[0], expected) def test_predigest_min_line_lenght(self): """Test small lines removal in the predigest process""" message = "This line is included\n" \ "not this\n" \ "This also" expected = ["Thislineisincluded", "Thisalso"] DataDigester(message.encode("utf8")) self.assertEqual(self.lines, expected) def test_predigest_atomic(self): """Test atomic messages (lines <= 4) in the predigest process""" message = "All this message\nShould be included\nIn the predigest" expected = ["Allthismessage", "Shouldbeincluded", "Inthepredigest"] DataDigester(message.encode("utf8")) self.assertEqual(self.lines, expected) def test_predigest_pieced(self): """Test pieced messages (lines > 4) in the predigest process""" message = "" for i in range(100): message += "Line%d test test test\n" % i expected = [] for i in [20, 21, 22, 60, 61, 62]: expected.append("Line%dtesttesttest" % i) DataDigester(message.encode("utf8")) self.assertEqual(self.lines, expected) class DigestTests(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.lines = [] def mock_digest_paylods(c, message): yield message.decode("utf8") self.real_digest_payloads = DataDigester.digest_payloads DataDigester.digest_payloads = mock_digest_paylods def tearDown(self): unittest.TestCase.tearDown(self) DataDigester.digest_payloads = self.real_digest_payloads def test_digest(self): message = b"That's some good ham right there" predigested = b"That'ssomegoodhamrightthere" digest = hashlib.sha1() digest.update(predigested) expected = digest.hexdigest() result = DataDigester(message).value self.assertEqual(result, expected) class MessageDigestTest(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) patch("pyzor.digest.DataDigester.normalize_html_part", return_value="normalized").start() self.config = { "get_content_maintype.return_value": "text", "get_content_charset.return_value": "utf8", "get_payload.return_value": Mock(), "get_payload.return_value.decode.return_value": "decoded" } def tearDown(self): unittest.TestCase.tearDown(self) patch.stopall() def check_msg(self): mock_part = Mock(**self.config) conf = {"walk.return_value": [mock_part]} mock_msg = Mock(**conf) return mock_part, mock_msg, list(DataDigester.digest_payloads(mock_msg)) def test_text(self): mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, ["decoded"]) expected = [call.decode('utf8', 'ignore')] payload = mock_part.get_payload.return_value payload.assert_has_calls(expected, True) def test_text_no_charset(self): self.config["get_content_charset.return_value"] = None mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, ["decoded"]) expected = [call.decode('ascii', 'ignore')] payload = mock_part.get_payload.return_value payload.assert_has_calls(expected) def test_text_quopri(self): self.config["get_content_charset.return_value"] = "quopri" mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, ["decoded"]) expected = [call.decode('quopri', 'strict')] payload = mock_part.get_payload.return_value payload.assert_has_calls(expected) def test_text_lookuperror(self): def _decode(encoding, errors): if encoding not in ("ascii",): raise LookupError() return "decoded" self.config["get_payload.return_value.decode.side_effect"] = _decode mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, ["decoded"]) expected = [call.decode('utf8', 'ignore'), call.decode('ascii', 'ignore')] payload = mock_part.get_payload.return_value payload.assert_has_calls(expected) def test_text_unicodeerror(self): self.config["get_payload.return_value.decode.side_effect"] = UnicodeError mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, []) expected = [call.decode('utf8', 'ignore'), call.decode('ascii', 'ignore')] payload = mock_part.get_payload.return_value payload.assert_has_calls(expected) def test_html(self): self.config["get_content_subtype.return_value"] = "html" mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, ["normalized"]) def test_multipart(self): self.config["get_content_maintype.return_value"] = "nottext" self.config["is_multipart.return_value"] = True mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, []) def test_nontext(self): self.config["get_content_maintype.return_value"] = "nottext" self.config["is_multipart.return_value"] = False mock_part, mock_msg, result = self.check_msg() self.assertEqual(result, [mock_part.get_payload.return_value]) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(HTMLStripperTests)) test_suite.addTest(unittest.makeSuite(PreDigestTests)) test_suite.addTest(unittest.makeSuite(DigestTests)) test_suite.addTest(unittest.makeSuite(MessageDigestTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_engines/000077500000000000000000000000001244204744500211255ustar00rootroot00000000000000pyzor-release-1-0-0/tests/unit/test_engines/__init__.py000066400000000000000000000012521244204744500232360ustar00rootroot00000000000000"""A suite of unit tests that verifies the correct behaviour of various functions/methods in the pyzord code. Note these tests the source of pyzor, not the version currently installed. """ import unittest def suite(): """Gather all the tests from this package in a test suite.""" import test_gdbm import test_mysql import test_redis import test_redis_v0 test_suite = unittest.TestSuite() test_suite.addTest(test_gdbm.suite()) test_suite.addTest(test_mysql.suite()) test_suite.addTest(test_redis.suite()) test_suite.addTest(test_redis_v0.suite()) return test_suite if __name__ == '__main__': unittest.main(defaultTest='suite') pyzor-release-1-0-0/tests/unit/test_engines/test_gdbm.py000066400000000000000000000122621244204744500234520ustar00rootroot00000000000000"""Test the pyzor.engines.gdbm_ module.""" import sys import time import unittest import threading from datetime import datetime, timedelta import pyzor.engines.gdbm_ import pyzor.engines.common class MockTimer(): def __init__(self, *args, **kwargs): pass def start(self): pass def setDaemon(self, daemon): pass class MockGdbmDB(dict): """Mock a gdbm database""" def firstkey(self): if not self.keys(): return None self.key_index = 1 return self.keys()[0] def nextkey(self, key): if len(self.keys()) <= self.key_index: return None else: self.key_index += 1 return self.keys()[self.key_index] def sync(self): pass def reorganize(self): pass class GdbmTest(unittest.TestCase): """Test the GdbmDBHandle class""" handler = pyzor.engines.gdbm_.GdbmDBHandle max_age = 60 * 60 * 24 * 30 * 4 r_count = 24 wl_count = 42 entered = datetime.now() - timedelta(days=10) updated = datetime.now() - timedelta(days=2) wl_entered = datetime.now() - timedelta(days=20) wl_updated = datetime.now() - timedelta(days=3) def setUp(self): unittest.TestCase.setUp(self) self.real_timer = threading.Timer threading.Timer = MockTimer self.db = MockGdbmDB() class MockGdbm(): @staticmethod def open(fn, mode): return self.db try: self.real_gdbm = pyzor.engines.gdbm_.gdbm except AttributeError: self.real_gdbm = None setattr(pyzor.engines.gdbm_, "gdbm", MockGdbm()) self.record = pyzor.engines.common.Record(self.r_count, self.wl_count, self.entered, self.updated, self.wl_entered, self.wl_updated) def tearDown(self): unittest.TestCase.tearDown(self) threading.Timer = self.real_timer pyzor.engines.gdbm_.gdbm = self.real_gdbm def record_as_str(self, record=None): if not record: record = self.record return ("1,%s,%s,%s,%s,%s,%s" % (record.r_count, record.r_entered, record.r_updated, record.wl_count, record.wl_entered, record.wl_updated)).encode("utf8") def test_set_item(self): """Test GdbmDBHandle.__setitem__""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" handle = self.handler(None, None, max_age=self.max_age) handle[digest] = self.record self.assertEqual(self.db[digest], self.record_as_str().decode("utf8")) def test_get_item(self): """Test GdbmDBHandle.__getitem__""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" handle = self.handler(None, None, max_age=self.max_age) self.db[digest] = self.record_as_str() result = handle[digest] self.assertEqual(self.record_as_str(result), self.record_as_str()) def test_items(self): """Test GdbmDBHandle.items()""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" handle = self.handler(None, None, max_age=self.max_age) self.db[digest] = self.record_as_str() key, result = handle.items()[0] self.assertEqual(key, digest) self.assertEqual(self.record_as_str(result), self.record_as_str()) def test_del_item(self): """Test GdbmDBHandle.__delitem__""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" handle = self.handler(None, None, max_age=self.max_age) self.db[digest] = self.record_as_str() del handle[digest] self.assertFalse(self.db.get(digest)) def test_reorganize_older(self): """Test GdbmDBHandle.start_reorganizing with older records""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.db[digest] = self.record_as_str() handle = self.handler(None, None, max_age=3600 * 24) self.assertFalse(self.db.get(digest)) def test_reorganize_older_no_max_age(self): """Test GdbmDBHandle.start_reorganizing with older records, but no max_age set. """ digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.db[digest] = self.record_as_str() handle = self.handler(None, None, max_age=None) self.assertEqual(self.db[digest], self.record_as_str()) def test_reorganize_fresh(self): """Test GdbmDBHandle.start_reorganizing with newer records""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" self.db[digest] = self.record_as_str() handle = self.handler(None, None, max_age=3600 * 24 * 3) self.assertEqual(self.db[digest], self.record_as_str()) class ThreadingGdbmTest(GdbmTest): """Test the GdbmDBHandle class""" handler = pyzor.engines.gdbm_.ThreadedGdbmDBHandle def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(GdbmTest)) test_suite.addTest(unittest.makeSuite(ThreadingGdbmTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_engines/test_mysql.py000066400000000000000000000153731244204744500237140ustar00rootroot00000000000000"""Test the pyzor.engines.mysql module.""" import unittest import threading from datetime import datetime, timedelta import pyzor.engines import pyzor.engines.mysql import pyzor.engines.common class MockTimer(): def __init__(self, *args, **kwargs): pass def start(self): pass def setDaemon(self, daemon): pass def make_MockMySQL(result, queries): class MockCursor(): def __init__(self): self.done = False def fetchone(self): if not self.done: self.done = True return result else: return None def fetchall(self): return [result] def execute(self, query, args=None): queries.append((query, args)) def close(self): pass class MockDB(): def cursor(self, *args, **kwargs): return MockCursor() def close(self): pass def commit(self): pass def autocommit(self, value): pass class MockMysql(): @staticmethod def connect(*args, **kwargs): return MockDB() class Error(Exception): pass class cursors: class SSCursor: pass return MockMysql class MySQLTest(unittest.TestCase): """Test the GdbmDBHandle class""" max_age = 60 * 60 * 24 * 30 * 4 r_count = 24 wl_count = 42 entered = datetime.now() - timedelta(days=10) updated = datetime.now() - timedelta(days=2) wl_entered = datetime.now() - timedelta(days=20) wl_updated = datetime.now() - timedelta(days=3) handler = pyzor.engines.mysql.MySQLDBHandle def setUp(self): unittest.TestCase.setUp(self) self.real_timer = threading.Timer threading.Timer = MockTimer self.record = pyzor.engines.common.Record(self.r_count, self.wl_count, self.entered, self.updated, self.wl_entered, self.wl_updated) self.response = self.record_unpack() self.queries = [] mock_MySQL = make_MockMySQL(self.response, self.queries) try: self.real_mysql = pyzor.engines.mysql.MySQLdb except AttributeError: self.real_mysql = None setattr(pyzor.engines.mysql, "MySQLdb", mock_MySQL) def tearDown(self): unittest.TestCase.tearDown(self) threading.Timer = self.real_timer pyzor.engines.mysql.MySQLdb = self.real_mysql def record_unpack(self, record=None): if not record: record = self.record return (record.r_count, record.wl_count, record.r_entered, record.r_updated, record.wl_entered, record.wl_updated) def test_reconnect(self): """Test MySQLDBHandle.__init__""" expected = "DELETE FROM testtable WHERE r_updated<%s" self.handler("testhost,testuser,testpass,testdb,testtable", None, max_age=self.max_age) self.assertEqual(self.queries[0][0], expected) def test_no_reorganize(self): self.handler("testhost,testuser,testpass,testdb,testtable", None, max_age=None) self.assertFalse(self.queries) def test_set_item(self): """Test MySQLDBHandle.__setitem__""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" expected = ("INSERT INTO testtable (digest, r_count, wl_count, " "r_entered, r_updated, wl_entered, wl_updated) " "VALUES (%s, %s, %s, %s, %s, %s, %s) ON " "DUPLICATE KEY UPDATE r_count=%s, wl_count=%s, " "r_entered=%s, r_updated=%s, wl_entered=%s, " "wl_updated=%s", (digest, self.r_count, self.wl_count, self.entered, self.updated, self.wl_entered, self.wl_updated, self.r_count, self.wl_count, self.entered, self.updated, self.wl_entered, self.wl_updated)) handle = self.handler("testhost,testuser,testpass,testdb,testtable", None, max_age=self.max_age) handle[digest] = self.record self.assertEqual(self.queries[1], expected) def test_get_item(self): """Test MySQLDBHandle.__getitem__""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" expected = ("SELECT r_count, wl_count, r_entered, r_updated, " "wl_entered, wl_updated FROM testtable WHERE digest=%s", (digest,)) handle = self.handler("testhost,testuser,testpass,testdb,testtable", None, max_age=self.max_age) result = handle[digest] self.assertEqual(self.queries[1], expected) self.assertEqual(self.record_unpack(result), self.record_unpack()) def test_del_item(self): """Test MySQLDBHandle.__detitem__""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" expected = ("DELETE FROM testtable WHERE digest=%s", (digest,)) handle = self.handler("testhost,testuser,testpass,testdb,testtable", None, max_age=self.max_age) del handle[digest] self.assertEqual(self.queries[1], expected) def test_items(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" expected = ("SELECT digest, r_count, wl_count, r_entered, r_updated, " "wl_entered, wl_updated FROM testtable", None) self.response = (digest, self.response) handle = self.handler("testhost,testuser,testpass,testdb,testtable", None, max_age=self.max_age) handle.items() self.assertEqual(self.queries[1], expected) def test_iter(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" expected = ("SELECT digest FROM testtable", None) self.response = (digest,) handle = self.handler("testhost,testuser,testpass,testdb,testtable", None, max_age=self.max_age) for d in handle: pass self.assertEqual(self.queries[1], expected) class ThreadedMySQLTest(MySQLTest): """Test the GdbmDBHandle class""" handler = pyzor.engines.mysql.ThreadedMySQLDBHandle class ProcessesMySQLTest(MySQLTest): """Test the GdbmDBHandle class""" handler = pyzor.engines.mysql.ProcessMySQLDBHandle def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(MySQLTest)) test_suite.addTest(unittest.makeSuite(ThreadedMySQLTest)) test_suite.addTest(unittest.makeSuite(ProcessesMySQLTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_engines/test_redis.py000066400000000000000000000176001244204744500236500ustar00rootroot00000000000000"""Test the pyzor.engines.gdbm_ module.""" import time import logging import unittest from datetime import datetime try: from unittest.mock import Mock, patch, call except ImportError: from mock import Mock, patch, call import pyzor.engines.redis_ import pyzor.engines.common class EncodingRedisTest(unittest.TestCase): """Test the RedisDBHandle class""" r_count = 24 wl_count = 42 entered = datetime(2014, 4, 23, 15, 41, 30) updated = datetime(2014, 4, 25, 17, 22, 25) wl_entered = datetime(2014, 2, 12, 11, 10, 55) wl_updated = datetime(2014, 3, 25, 5, 1, 50) def setUp(self): unittest.TestCase.setUp(self) self.record = pyzor.engines.common.Record(self.r_count, self.wl_count, self.entered, self.updated, self.wl_entered, self.wl_updated) self.entered_st = int(time.mktime(self.entered.timetuple())) self.updated_st = int(time.mktime(self.updated.timetuple())) self.wl_entered_st = int(time.mktime(self.wl_entered.timetuple())) self.wl_updated_st = int(time.mktime(self.wl_updated.timetuple())) def compare_records(self, r1, r2): attrs = ("r_count", "r_entered", "r_updated", "wl_count", "wl_entered", "wl_updated") self.assertTrue(all(getattr(r1, attr) == getattr(r2, attr) for attr in attrs)) def tearDown(self): unittest.TestCase.tearDown(self) def test_encode_record(self): expected = { "r_count": 24, "r_entered": self.entered_st, "r_updated": self.updated_st, "wl_count": 42, "wl_entered": self.wl_entered_st, "wl_updated": self.wl_updated_st } result = pyzor.engines.redis_.RedisDBHandle._encode_record(self.record) self.assertEqual(result, expected) def test_encode_record_no_date(self): expected = { "r_count": 24, "r_entered": self.entered_st, "r_updated": 0, "wl_count": 42, "wl_entered": self.wl_entered_st, "wl_updated": self.wl_updated_st } self.record.r_updated = None result = pyzor.engines.redis_.RedisDBHandle._encode_record(self.record) self.assertEqual(result, expected) def test_encode_record_no_white(self): expected = { "r_count": 24, "r_entered": self.entered_st, "r_updated": self.updated_st, "wl_count": 0, "wl_entered": 0, "wl_updated": 0 } self.record.wl_count = 0 self.record.wl_entered = None self.record.wl_updated = None result = pyzor.engines.redis_.RedisDBHandle._encode_record(self.record) self.assertEqual(result, expected) def test_decode_record(self): encoded = { b"r_count": 24, b"r_entered": self.entered_st, b"r_updated": self.updated_st, b"wl_count": 42, b"wl_entered": self.wl_entered_st, b"wl_updated": self.wl_updated_st } result = pyzor.engines.redis_.RedisDBHandle._decode_record(encoded) self.compare_records(result, self.record) def test_decode_record_no_date(self): encoded = { b"r_count": 24, b"r_entered": self.entered_st, b"r_updated": 0, b"wl_count": 42, b"wl_entered": self.wl_entered_st, b"wl_updated": self.wl_updated_st } result = pyzor.engines.redis_.RedisDBHandle._decode_record(encoded) self.record.r_updated = None self.compare_records(result, self.record) def test_decode_record_no_white(self): encoded = { b"r_count": 24, b"r_entered": self.entered_st, b"r_updated": self.updated_st, b"wl_count": 0, b"wl_entered": 0, b"wl_updated": 0 } result = pyzor.engines.redis_.RedisDBHandle._decode_record(encoded) self.record.wl_count = 0 self.record.wl_entered = None self.record.wl_updated = None self.compare_records(result, self.record) class RedisTest(unittest.TestCase): max_age = 60 * 60 def setUp(self): unittest.TestCase.setUp(self) logger = logging.getLogger("pyzord") logger.addHandler(logging.NullHandler()) self.mredis = patch("pyzor.engines.redis_.redis", create=True).start() patch("pyzor.engines.redis_.RedisDBHandle._encode_record", side_effect=lambda x: x).start() patch("pyzor.engines.redis_.RedisDBHandle._decode_record", side_effect=lambda x: x).start() def tearDown(self): unittest.TestCase.tearDown(self) patch.stopall() def test_init(self): expected = {"host": "example.com", "port": 6387, "password": "passwd", "db": 5, } db = pyzor.engines.redis_.RedisDBHandle("example.com,6387,passwd,5", None) self.mredis.StrictRedis.assert_called_with(**expected) def test_init_defaults(self): expected = {"host": "localhost", "port": 6379, "password": None, "db": 0, } db = pyzor.engines.redis_.RedisDBHandle(",,,", None) self.mredis.StrictRedis.assert_called_with(**expected) def test_set(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" value = "record test" db = pyzor.engines.redis_.RedisDBHandle(",,,", None) db[digest] = value expected = ("pyzord.digest_v1.%s" % digest, value) self.mredis.StrictRedis.return_value.hmset.assert_called_with(*expected) def test_set_max_age(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" value = "record test" db = pyzor.engines.redis_.RedisDBHandle(",,,", None, max_age=self.max_age) db[digest] = value expected1 = ("pyzord.digest_v1.%s" % digest, value) expected2 = ("pyzord.digest_v1.%s" % digest, self.max_age) self.mredis.StrictRedis.return_value.hmset.assert_called_with(*expected1) self.mredis.StrictRedis.return_value.expire.assert_called_with(*expected2) def test_get(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" db = pyzor.engines.redis_.RedisDBHandle(",,,", None) result = db[digest] expected = ("pyzord.digest_v1.%s" % digest,) self.mredis.StrictRedis.return_value.hgetall.assert_called_with(*expected) def test_items(self): patch("pyzor.engines.redis_.redis.StrictRedis.return_value.keys", return_value=["2aedaac999d71421c9ee49b9d81f627a7bc570aa"]).start() digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" db = pyzor.engines.redis_.RedisDBHandle(",,,", None) db.items()[0] expected = ("pyzord.digest_v1.%s" % digest,) self.mredis.StrictRedis.return_value.keys.assert_called_with("pyzord.digest_v1.*") self.mredis.StrictRedis.return_value.hgetall.assert_called_with(*expected) def test_delete(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" db = pyzor.engines.redis_.RedisDBHandle(",,,", None) del db[digest] expected = ("pyzord.digest_v1.%s" % digest,) self.mredis.StrictRedis.return_value.delete.assert_called_with(*expected) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(EncodingRedisTest)) test_suite.addTest(unittest.makeSuite(RedisTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_engines/test_redis_v0.py000066400000000000000000000173321244204744500242570ustar00rootroot00000000000000"""Test the pyzor.engines.gdbm_ module.""" import unittest from datetime import datetime import pyzor.engines.redis_v0 import pyzor.engines.common class EncodingRedisTest(unittest.TestCase): """Test the RedisDBHandle class""" r_count = 24 wl_count = 42 entered = datetime(2014, 4, 23, 15, 41, 30) updated = datetime(2014, 4, 25, 17, 22, 25) wl_entered = datetime(2014, 2, 12, 11, 10, 55) wl_updated = datetime(2014, 3, 25, 5, 1, 50) def setUp(self): unittest.TestCase.setUp(self) self.record = pyzor.engines.common.Record(self.r_count, self.wl_count, self.entered, self.updated, self.wl_entered, self.wl_updated) def compare_records(self, r1, r2): attrs = ("r_count", "r_entered", "r_updated", "wl_count", "wl_entered", "wl_updated") self.assertTrue(all(getattr(r1, attr) == getattr(r2, attr) for attr in attrs)) def tearDown(self): unittest.TestCase.tearDown(self) def test_encode_record(self): expected = ("24,2014-04-23 15:41:30,2014-04-25 17:22:25," "42,2014-02-12 11:10:55,2014-03-25 05:01:50").encode() result = pyzor.engines.redis_v0.RedisDBHandle._encode_record(self.record) self.assertEqual(result, expected) def test_encode_record_no_date(self): expected = ("24,2014-04-23 15:41:30,," "42,2014-02-12 11:10:55,2014-03-25 05:01:50").encode() self.record.r_updated = None result = pyzor.engines.redis_v0.RedisDBHandle._encode_record(self.record) self.assertEqual(result, expected) def test_encode_record_no_white(self): expected = ("24,2014-04-23 15:41:30,2014-04-25 17:22:25," "0,,").encode() self.record.wl_count = 0 self.record.wl_entered = None self.record.wl_updated = None result = pyzor.engines.redis_v0.RedisDBHandle._encode_record(self.record) self.assertEqual(result, expected) def test_decode_record(self): encoded = ("24,2014-04-23 15:41:30,2014-04-25 17:22:25," "42,2014-02-12 11:10:55,2014-03-25 05:01:50").encode() result = pyzor.engines.redis_v0.RedisDBHandle._decode_record(encoded) self.compare_records(result, self.record) def test_decode_record_no_date(self): encoded = ("24,2014-04-23 15:41:30,," "42,2014-02-12 11:10:55,2014-03-25 05:01:50").encode() result = pyzor.engines.redis_v0.RedisDBHandle._decode_record(encoded) self.record.r_updated = None self.compare_records(result, self.record) def test_decode_record_no_white(self): encoded = ("24,2014-04-23 15:41:30,2014-04-25 17:22:25," "0,,").encode() result = pyzor.engines.redis_v0.RedisDBHandle._decode_record(encoded) self.record.wl_count = 0 self.record.wl_entered = None self.record.wl_updated = None self.compare_records(result, self.record) def make_MockRedis(commands): class MockStrictRedis(): def __init__(self, *args, **kwargs): commands.append(("init", args, kwargs)) def set(self, *args, **kwargs): commands.append(("set", args, kwargs)) def setex(self, *args, **kwargs): commands.append(("setex", args, kwargs)) def get(self, *args, **kwargs): commands.append(("get", args, kwargs)) def delete(self, *args, **kwargs): commands.append(("delete", args, kwargs)) def keys(self, *args, **kwargs): commands.append(("keys", args, kwargs)) yield "pyzord.digest.2aedaac999d71421c9ee49b9d81f627a7bc570aa" class MockError(Exception): pass class exceptions(): def __init__(self): self.RedisError = MockError class MockRedis(): def __init__(self): self.StrictRedis = MockStrictRedis self.exceptions = exceptions() return MockRedis() mock_encode_record = lambda s, x: x mock_decode_record = lambda s, x: x class RedisTest(unittest.TestCase): max_age = 60 * 60 def setUp(self): unittest.TestCase.setUp(self) self.commands = [] try: self.real_redis = pyzor.engines.redis_v0.redis except AttributeError: self.real_redis = None self.real_encode = pyzor.engines.redis_v0.RedisDBHandle._encode_record self.real_decode = pyzor.engines.redis_v0.RedisDBHandle._decode_record setattr(pyzor.engines.redis_v0, "redis", make_MockRedis(self.commands)) pyzor.engines.redis_v0.RedisDBHandle._encode_record = mock_encode_record pyzor.engines.redis_v0.RedisDBHandle._decode_record = mock_decode_record def tearDown(self): unittest.TestCase.tearDown(self) pyzor.engines.redis_v0.redis = self.real_redis pyzor.engines.redis_v0.RedisDBHandle._encode_record = self.real_encode pyzor.engines.redis_v0.RedisDBHandle._decode_record = self.real_decode def test_init(self): expected = {"host": "example.com", "port": 6387, "password": "passwd", "db": 5, } db = pyzor.engines.redis_v0.RedisDBHandle("example.com,6387,passwd,5", None) self.assertEqual(self.commands[0], ("init", (), expected)) def test_init_defaults(self): expected = {"host": "localhost", "port": 6379, "password": None, "db": 0, } db = pyzor.engines.redis_v0.RedisDBHandle(",,,", None) self.assertEqual(self.commands[0], ("init", (), expected)) def test_set(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" value = "record test" db = pyzor.engines.redis_v0.RedisDBHandle(",,,", None) db[digest] = value expected = ("pyzord.digest.%s" % digest, value) self.assertEqual(self.commands[1], ("set", expected, {})) def test_set_max_age(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" value = "record test" db = pyzor.engines.redis_v0.RedisDBHandle(",,,", None, max_age=self.max_age) db[digest] = value expected = ("pyzord.digest.%s" % digest, self.max_age, value) self.assertEqual(self.commands[1], ("setex", expected, {})) def test_get(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" db = pyzor.engines.redis_v0.RedisDBHandle(",,,", None) result = db[digest] expected = ("pyzord.digest.%s" % digest,) self.assertEqual(self.commands[1], ("get", expected, {})) def test_items(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" db = pyzor.engines.redis_v0.RedisDBHandle(",,,", None) db.items()[0] expected = ("pyzord.digest.%s" % digest,) self.assertEqual(self.commands[1], ("keys", ("pyzord.digest.*",), {})) self.assertEqual(self.commands[2], ("get", expected, {})) def test_delete(self): digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" db = pyzor.engines.redis_v0.RedisDBHandle(",,,", None) del db[digest] expected = ("pyzord.digest.%s" % digest,) self.assertEqual(self.commands[1], ("delete", expected, {})) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(EncodingRedisTest)) test_suite.addTest(unittest.makeSuite(RedisTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_forwarder.py000066400000000000000000000043521244204744500220460ustar00rootroot00000000000000"""Test the pyzor.forwarder module """ import time import unittest import threading from mock import call, Mock import pyzor.forwarder class ForwarderTest(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) def tearDown(self): unittest.TestCase.tearDown(self) def test_queue(self): client = Mock() servlist = [] max_qsize = 10 forwarder = pyzor.forwarder.Forwarder(client, servlist, max_queue_size=max_qsize) for _ in xrange(max_qsize * 2): forwarder.queue_forward_request('975422c090e7a43ab7c9bf0065d5b661259e6d74') self.assertGreater(forwarder.forward_queue.qsize(), 0, 'queue insert failed') self.assertLessEqual(forwarder.forward_queue.qsize(), max_qsize, 'queue overload') self.assertEqual(forwarder.forward_queue.qsize(), max_qsize, 'queue should be full at this point') t = threading.Thread(target=forwarder._forward_loop) t.start() time.sleep(1) self.assertEqual(forwarder.forward_queue.qsize(), 0, 'queue should be empty') forwarder.stop_forwarding() t.join(5) self.assertFalse(t.isAlive(), 'forward thread did not end') def test_remote_servers(self): client = Mock() digest = '975422c090e7a43ab7c9bf0065d5b661259e6d74' servlist = [("test1.example.com", 24441), ("test2.example.com", 24442)] forwarder = pyzor.forwarder.Forwarder(client, servlist) forwarder.queue_forward_request(digest) forwarder.queue_forward_request(digest, whitelist=True) forwarder.start_forwarding() time.sleep(2) forwarder.stop_forwarding() client.report.assert_has_calls([call(digest, servlist[0]), call(digest, servlist[1])]) client.whitelist.assert_has_calls([call(digest, servlist[0]), call(digest, servlist[1])]) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(ForwarderTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/unit/test_server.py000066400000000000000000000335171244204744500213660ustar00rootroot00000000000000"""Test the pyzor.server module """ import io import sys import time import logging import unittest import SocketServer from datetime import datetime, timedelta try: from unittest.mock import patch except ImportError: from mock import patch import pyzor.server import pyzor.engines.common class MockServer(): """Mocks the pyzor.server.Server class""" def __init__(self): self.log = logging.getLogger("pyzord") self.usage_log = logging.getLogger("pyzord-usage") self.log.addHandler(logging.NullHandler()) self.usage_log.addHandler(logging.NullHandler()) self.forwarder = None self.one_step = False class MockDatagramRequestHandler(): """ Mock the SocketServer.DatagramRequestHand.""" def __init__(self, headers, database=None, acl=None, accounts=None): """Initiates an request handler and set's the data in `headers` as the request. Also set's the database, acl and accounts for the MockServer. This will be set as base class for RequestHandler. """ self.rfile = io.BytesIO() self.wfile = io.BytesIO() for i, j in headers.iteritems(): self.rfile.write(("%s: %s\n" % (i, j)).encode("utf8")) self.rfile.seek(0) self.packet = None self.client_address = ["127.0.0.1"] # Setup MockServer data self.server = MockServer() self.server.database = database if acl: self.server.acl = acl else: self.server.acl = {pyzor.anonymous_user: ("check", "report", "ping", "pong", "info", "whitelist",)} self.server.accounts = accounts self.handle() def handle(self): pass class RequestHandlerTest(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.real_drh = SocketServer.DatagramRequestHandler SocketServer.DatagramRequestHandler = MockDatagramRequestHandler pyzor.server.RequestHandler.__bases__ = (MockDatagramRequestHandler,) # setup the basic values for request and response self.request = {"User": pyzor.anonymous_user, "Time": str(int(time.time())), "PV": str(pyzor.proto_version), "Thread": "3597"} self.expected_response = {"Code": "200", "Diag": "OK", "PV": str(pyzor.proto_version), "Thread": "3597"} def tearDown(self): unittest.TestCase.tearDown(self) SocketServer.DatagramRequestHandler = self.real_drh pyzor.server.RequestHandler.__bases__ = (self.real_drh,) patch.stopall() def check_response(self, handler): """Checks if the response from the handler is equal to the expected response. """ handler.wfile.seek(0) response = handler.wfile.read() response = response.decode("utf8").replace("\n\n", "\n") result = {} try: for line in response.splitlines(): key = line.split(":", 1)[0].strip() value = line.split(":")[1].strip() result[key] = value except (IndexError, TypeError) as e: self.fail("Error parsing %r: %s" % (response, e)) self.assertEqual(result, self.expected_response) def timestamp(self, time_obj): if not time_obj: return 0 else: return str(int(time.mktime(time_obj.timetuple()))) def test_ping(self): """Tests the ping command handler""" self.request["Op"] = "ping" handler = pyzor.server.RequestHandler(self.request) self.check_response(handler) def test_pong(self): """Tests the pong command handler""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {digest: pyzor.engines.common.Record(24, 42)} self.request["Op"] = "pong" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.expected_response["Count"] = str(sys.maxint) self.expected_response["WL-Count"] = "0" self.check_response(handler) def test_check(self): """Tests the check command handler""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {digest: pyzor.engines.common.Record(24, 42)} self.request["Op"] = "check" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.expected_response["Count"] = "24" self.expected_response["WL-Count"] = "42" self.check_response(handler) def test_check_new(self): """Tests the check command handler with a new record""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {} self.request["Op"] = "check" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.expected_response["Count"] = "0" self.expected_response["WL-Count"] = "0" self.check_response(handler) def test_info(self): """Tests the info command handler""" entered = datetime.now() - timedelta(days=10) updated = datetime.now() wl_entered = datetime.now() - timedelta(days=20) wl_updated = datetime.now() - timedelta(days=2) digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {digest: pyzor.engines.common.Record(24, 42, entered, updated, wl_entered, wl_updated)} self.request["Op"] = "info" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.expected_response["Count"] = "24" self.expected_response["WL-Count"] = "42" self.expected_response["Entered"] = self.timestamp(entered) self.expected_response["Updated"] = self.timestamp(updated) self.expected_response["WL-Entered"] = self.timestamp(wl_entered) self.expected_response["WL-Updated"] = self.timestamp(wl_updated) self.check_response(handler) def test_info_new(self): """Tests the info command handler with a new record""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {} self.request["Op"] = "info" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.expected_response["Count"] = "0" self.expected_response["WL-Count"] = "0" self.expected_response["Entered"] = "0" self.expected_response["Updated"] = "0" self.expected_response["WL-Entered"] = "0" self.expected_response["WL-Updated"] = "0" self.check_response(handler) def test_report(self): """Tests the report command handler""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {digest: pyzor.engines.common.Record(24, 42)} self.request["Op"] = "report" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.check_response(handler) self.assertEqual(database[digest].r_count, 25) def test_report_new(self): """Tests the report command handler with a new record""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {} self.request["Op"] = "report" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.check_response(handler) self.assertEqual(database[digest].r_count, 1) def test_whitelist(self): """Tests the whitelist command handler""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {digest: pyzor.engines.common.Record(24, 42)} self.request["Op"] = "whitelist" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.check_response(handler) self.assertEqual(database[digest].wl_count, 43) def test_whitelist_new(self): """Tests the whitelist command handler with a new record""" digest = "2aedaac999d71421c9ee49b9d81f627a7bc570aa" database = {} self.request["Op"] = "whitelist" self.request["Op-Digest"] = digest handler = pyzor.server.RequestHandler(self.request, database) self.check_response(handler) self.assertEqual(database[digest].wl_count, 1) def test_handle_no_version(self): """Tests handling an request with no version specified""" self.request["Op"] = "ping" del self.request["PV"] handler = pyzor.server.RequestHandler(self.request) self.expected_response["Code"] = "400" self.expected_response["Diag"] = "Bad request" self.check_response(handler) def test_handle_unsupported_version(self): """Tests handling an request with an unsupported version specified""" self.request["Op"] = "ping" self.request["PV"] = str(pyzor.proto_version + 2) handler = pyzor.server.RequestHandler(self.request) self.expected_response["Code"] = "505" self.expected_response["Diag"] = "Version Not Supported" self.check_response(handler) def test_handle_not_implemented(self): """Tests handling an request with an unimplemented command""" self.request["Op"] = "notimplemented" acl = {pyzor.anonymous_user: "notimplemented"} handler = pyzor.server.RequestHandler(self.request, acl=acl) self.expected_response["Code"] = "501" self.expected_response["Diag"] = "Not implemented" self.check_response(handler) def test_handle_unauthorized(self): """Tests handling an request with an unauthorized command""" self.request["Op"] = "report" acl = {pyzor.anonymous_user: ("ping", "check")} handler = pyzor.server.RequestHandler(self.request, acl=acl) self.expected_response["Code"] = "403" self.expected_response["Diag"] = "Forbidden" self.check_response(handler) def test_handle_account(self): """Tests handling an request where user is not anonymous""" self.request["Op"] = "ping" self.request["User"] = "testuser" acl = {"testuser": ("ping", "check")} accounts = {"testuser": "testkey"} mock_vs = lambda x, y: None real_vs = pyzor.account.verify_signature pyzor.account.verify_signature = mock_vs try: handler = pyzor.server.RequestHandler(self.request, acl=acl, accounts=accounts) self.check_response(handler) finally: pyzor.account.verify_signature = real_vs def test_handle_unknown_account(self): """Tests handling an request where user is unkwown""" self.request["Op"] = "ping" self.request["User"] = "testuser" acl = {"testuser": ("ping", "check")} accounts = {} self.expected_response["Code"] = "401" self.expected_response["Diag"] = "Unauthorized" def mock_vs(x, y): pass real_vs = pyzor.account.verify_signature pyzor.account.verify_signature = mock_vs try: handler = pyzor.server.RequestHandler(self.request, acl=acl, accounts=accounts) self.check_response(handler) finally: pyzor.account.verify_signature = real_vs def test_handle_invalid_signature(self): """Tests handling an request where user key is invalid""" self.request["Op"] = "ping" self.request["User"] = "testuser" acl = {"testuser": ("ping", "check")} accounts = {"testuser": ("ping", "check")} self.expected_response["Code"] = "401" self.expected_response["Diag"] = "Unauthorized" def mock_vs(x, y): raise pyzor.SignatureError("Invalid signature.") real_vs = pyzor.account.verify_signature pyzor.account.verify_signature = mock_vs try: handler = pyzor.server.RequestHandler(self.request, acl=acl, accounts=accounts) self.check_response(handler) finally: pyzor.account.verify_signature = real_vs def test_invalid_pv(self): self.request["Op"] = "ping" self.request["PV"] = "ab2.13" handler = pyzor.server.RequestHandler(self.request) self.expected_response["Code"] = "400" self.expected_response["Diag"] = "Bad request" self.check_response(handler) def test_uncaught_exception(self): patch("pyzor.server.RequestHandler._really_handle", side_effect=Exception("test")).start() self.request["Op"] = "ping" handler = pyzor.server.RequestHandler(self.request) self.expected_response["Code"] = "500" self.expected_response["Diag"] = "Internal Server Error" del self.expected_response["Thread"] self.check_response(handler) class ServerTest(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.mock_config = patch("pyzor.config").start() def tearDown(self): unittest.TestCase.tearDown(self) patch.stopall() def test_server(self): pyzor.server.Server(("127.0.0.1", 24441), {}, "passwd_fn", "access_fn", None) def suite(): """Gather all the tests from this module in a test suite.""" test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(RequestHandlerTest)) test_suite.addTest(unittest.makeSuite(ServerTest)) return test_suite if __name__ == '__main__': unittest.main() pyzor-release-1-0-0/tests/util/000077500000000000000000000000001244204744500164345ustar00rootroot00000000000000pyzor-release-1-0-0/tests/util/__init__.py000066400000000000000000000334521244204744500205540ustar00rootroot00000000000000"""This package contains various utilities use in the pyzor tests.""" import os import sys import time import redis import shutil import unittest import subprocess from datetime import datetime try: from unittest.mock import mock_open as _mock_open except ImportError: from mock import mock_open as _mock_open import pyzor.client def mock_open(mock=None, read_data=""): mock = _mock_open(mock, read_data) mock.return_value.__iter__ = lambda x: iter(read_data.splitlines()) return mock msg = """Newsgroups: Date: Wed, 10 Apr 2002 22:23:51 -0400 (EDT) From: Frank Tobin Fcc: sent-mail Message-ID: <20020410222350.E16178@palanthas.neverending.org> X-Our-Headers: X-Bogus,Anon-To X-Bogus: aaron7@neverending.org MIME-Version: 1.0 Content-Type: TEXT/PLAIN; charset=US-ASCII Test Email """ digest = "7421216f915a87e02da034cc483f5c876e1a1338" _dt_decode = lambda x: None if x == 'None' else datetime.strptime(x, "%a %b %d %H:%M:%S %Y") class PyzorTestBase(unittest.TestCase): """Test base that starts the pyzord daemon in setUpClass with specified arguments. The daemon is killed in tearDownClass. This also create the necessary files and the homedir. """ pyzord = None _args = {"homedir": "--homedir", "engine": "-e", "dsn": "--dsn", "address": "-a", "port": "-p", "threads": "--threads", "max_threads": "--max-threads", "processes": "--processes", "max_processes": "--max-processes", "db_connections": "--db-connections", "password_file": "--password-file", "access_file": "--access-file", "cleanup_age": "--cleanup-age", "log_file": "--log-file", "detach": "--detach", "prefork": "--pre-fork", } homedir = "./pyzor-test/" threads = "False" access_file = "pyzord.access" password_file = "pyzord.passwd" log_file = "pyzord-test.log" dsn = "localhost,,,10" engine = "redis" access = """check report ping pong info whitelist : alice : deny check report ping pong info whitelist : bob : allow ALL : dan : allow pong info whitelist : dan : deny """ passwd = """alice : fc7f1cad729b5f3862b2ef192e2d9e0d0d4bd515 bob : cf88277c5d4abdc0a3f56f416011966d04a3f462 dan : c1a50281fc43e860fe78c16c73b9618ada59f959 """ servers = """127.0.0.1:9999 """ accounts_alice = """127.0.0.1 : 9999 : alice : d28f86151e80a9accba4a4eba81c460532384cd6,fc7f1cad729b5f3862b2ef192e2d9e0d0d4bd515 """ accounts_bob = """127.0.0.1 : 9999 : bob : de6ef568787256bf5f55909dc0c398e49b5c9808,cf88277c5d4abdc0a3f56f416011966d04a3f462 """ accounts_chuck = """127.0.0.1 : 9999 : bob : de6ef568787256bf5f55909dc0c398e49b5c9808,af88277c5d4abdc0a3f56f416011966d04a3f462 """ accounts_dan = """127.0.0.1 : 9999 : dan : 1cc2efa77d8833d83556e0cc4fa617c64eebc7fb,c1a50281fc43e860fe78c16c73b9618ada59f959 """ @classmethod def write_homedir_file(cls, name, content): if not name or not content: return with open(os.path.join(cls.homedir, name), "w") as f: f.write(content) @classmethod def setUpClass(cls): super(PyzorTestBase, cls).setUpClass() try: os.mkdir(cls.homedir) except OSError: pass cls.write_homedir_file(cls.access_file, cls.access) cls.write_homedir_file(cls.password_file, cls.passwd) cls.write_homedir_file(cls.password_file, cls.passwd) cls.write_homedir_file("servers", cls.servers) cls.write_homedir_file("alice", cls.accounts_alice) cls.write_homedir_file("bob", cls.accounts_bob) cls.write_homedir_file("chuck", cls.accounts_chuck) cls.write_homedir_file("dan", cls.accounts_dan) args = ["pyzord"] for key, value in cls._args.iteritems(): option = getattr(cls, key, None) if option: args.append(value) args.append(option) cls.pyzord = [] for line in cls.servers.splitlines(): line = line.strip() if not line: continue addr, port = line.rsplit(":", 1) cls.pyzord.append(subprocess.Popen(args + ["-a", addr, "-p", port])) time.sleep(1) # allow time to initialize server def setUp(self): unittest.TestCase.setUp(self) self.client_args = {"--homedir": self.homedir, "--servers-file": "servers", "-t": None, # timeout "-r": None, # report threshold "-w": None, # whitelist threshold "-s": None, # style } def tearDown(self): unittest.TestCase.tearDown(self) @classmethod def tearDownClass(cls): super(PyzorTestBase, cls).tearDownClass() for pyzord in cls.pyzord: pyzord.terminate() pyzord.wait() shutil.rmtree(cls.homedir, True) redis.StrictRedis(db=10).flushdb() def check_pyzor(self, cmd, user, input=None, code=None, exit_code=None, counts=()): """Call the pyzor client with the specified args from self.client_args and verifies the response. """ args = ["pyzor"] if user: args.append("--accounts-file") args.append(user) for key, value in self.client_args.iteritems(): if value: args.append(key) args.append(value) args.append(cmd) pyzor = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if input: stdout, stderr = pyzor.communicate(input.encode("utf8")) else: stdout, stderr = pyzor.communicate() if stderr: self.fail(stderr) if code is not None: try: stdout = stdout.decode("utf8") results = stdout.strip().split("\t") status = eval(results[1]) except Exception as e: self.fail("Parsing error: %s of %r" % (e, stdout)) self.assertEqual(status[0], code, status) if counts: self.assertEqual(counts, (int(results[2]), int(results[3]))) if exit_code is not None: self.assertEqual(exit_code, pyzor.returncode) return stdout def check_pyzor_multiple(self, cmd, user, input=None, code=None, exit_code=None, counts=()): """Call the pyzor client with the specified args from self.client_args and verifies the response. """ args = ["pyzor"] if user: args.append("--accounts-file") args.append(user) for key, value in self.client_args.iteritems(): if value: args.append(key) args.append(value) args.append(cmd) pyzor = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if input: stdout, stderr = pyzor.communicate(input.encode("utf8")) else: stdout, stderr = pyzor.communicate() if stderr: self.fail(stderr) stdout = stdout.decode("utf8") for i, line in enumerate(stdout.splitlines()): try: line = line.strip() if not line: continue results = line.strip().split("\t") except Exception as e: self.fail("Parsing error: %s of %r" % (e, stdout)) if code is not None: try: status = eval(results[1]) except Exception as e: self.fail("Parsing error: %s of %r" % (e, stdout)) self.assertEqual(status[0], code[i], status) if counts: self.assertEqual((int(results[2]), int(results[3])), counts[i]) if exit_code is not None: self.assertEqual(exit_code, pyzor.returncode) return stdout def check_digest(self, digest, address, counts=(0, 0)): result = self.client.check(digest, address) self.assertEqual((int(result["Count"]), int(result["WL-Count"])), counts) return result def get_record(self, input, user="bob"): """Uses `pyzor info` to get the record data.""" stdout = self.check_pyzor("info", user, input, code=200, exit_code=0) info = stdout.splitlines()[1:] record = {} try: for line in info: line = line.strip() if not line: continue key, value = line.split(":", 1) record[key.strip()] = value.strip() except Exception as e: self.fail("Error parsing %r: %s" % (info, e)) return record def check_fuzzy_date(self, date1, date2=None, seconds=10): """Check if the given date is almost equal to now.""" date1 = _dt_decode(date1) if not date2: date2 = datetime.now() delta = abs((date2 - date1).total_seconds()) if delta > seconds: self.fail("Delta %s is too big: %s, %s" % (delta , date1, date2)) class PyzorTest(object): """MixIn class for PyzorTestBase that performs a series of basic tests.""" def test_ping(self): self.check_pyzor("ping", "bob") def test_pong(self): input = "Test1 pong1 Test2" self.check_pyzor("pong", "bob", input=input, code=200, exit_code=0, counts=(sys.maxint, 0)) def test_check(self): input = "Test1 check1 Test2" self.check_pyzor("check", "bob", input=input, code=200, exit_code=1, counts=(0, 0)) r = self.get_record(input) self.assertEqual(r["Count"], "0") def test_report(self): input = "Test1 report1 Test2" self.check_pyzor("report", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=0, counts=(1, 0)) r = self.get_record(input) self.assertEqual(r["Count"], "1") self.check_fuzzy_date(r["Entered"]) def test_report_update(self): input = "Test1 report update1 Test2" self.check_pyzor("report", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=0, counts=(1, 0)) time.sleep(1) self.check_pyzor("report", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=0, counts=(2, 0)) r = self.get_record(input) self.assertEqual(r["Count"], "2") self.assertNotEqual(r["Entered"], r["Updated"]) self.check_fuzzy_date(r["Updated"]) def test_whitelist(self): input = "Test1 white list1 Test2" self.check_pyzor("whitelist", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=1, counts=(0, 1)) r = self.get_record(input) self.assertEqual(r["WL-Count"], "1") self.check_fuzzy_date(r["WL-Entered"]) def test_whitelist_update(self): input = "Test1 white list update1 Test2" self.check_pyzor("whitelist", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=1, counts=(0, 1)) time.sleep(1) self.check_pyzor("whitelist", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=1, counts=(0, 2)) r = self.get_record(input) self.assertEqual(r["WL-Count"], "2") self.assertNotEqual(r["WL-Entered"], r["WL-Updated"]) self.check_fuzzy_date(r["WL-Updated"]) def test_report_whitelist(self): input = "Test1 white list report1 Test2" self.check_pyzor("whitelist", "bob", input=input, code=200, exit_code=0) self.check_pyzor("report", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=1, counts=(1, 1)) r = self.get_record(input) self.assertEqual(r["Count"], "1") self.check_fuzzy_date(r["Entered"]) self.assertEqual(r["WL-Count"], "1") self.check_fuzzy_date(r["WL-Entered"]) def test_report_whitelist_update(self): input = "Test1 white list report update1 Test2" self.check_pyzor("whitelist", "bob", input=input, code=200, exit_code=0) self.check_pyzor("report", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=1, counts=(1, 1)) time.sleep(1) self.check_pyzor("whitelist", "bob", input=input, code=200, exit_code=0) self.check_pyzor("report", "bob", input=input, code=200, exit_code=0) self.check_pyzor("check", "bob", input=input, code=200, exit_code=1, counts=(2, 2)) r = self.get_record(input) self.assertEqual(r["Count"], "2") self.assertNotEqual(r["Entered"], r["Updated"]) self.check_fuzzy_date(r["Updated"]) self.assertEqual(r["WL-Count"], "2") self.assertNotEqual(r["WL-Entered"], r["WL-Updated"]) self.check_fuzzy_date(r["WL-Updated"]) pyzor-release-1-0-0/web/000077500000000000000000000000001244204744500150725ustar00rootroot00000000000000pyzor-release-1-0-0/web/application.py000077500000000000000000000204231244204744500177530ustar00rootroot00000000000000#! /usr/bin/env python import os import email import socket import logging import smtplib import datetime import email.utils import ConfigParser import email.mime.base import email.mime.text import email.mime.multipart import flask from flask_wtf.form import Form from flask.views import MethodView from wtforms.fields.simple import TextField, SubmitField, TextAreaField from flask_wtf.html5 import EmailField from flask_wtf.file import FileField from flask_wtf.recaptcha.fields import RecaptchaField from wtforms.validators import required, length try: from raven.contrib.flask import Sentry from raven.handlers.logging import SentryHandler except ImportError: pass import pyzor import pyzor.digest import pyzor.client MSG_TEMPLATE_TXT = """ Whitelist request: - Date: %s - Name: %%(name)s - Email: %%(email)s - Digest: %%(digest)s - Request IP: %%(ip)s =============== %%(comment)s =============== Pyzor Version: %s """ % (datetime.datetime.utcnow(), pyzor.__version__) def load_configuration(): """Load server-specific configuration settings.""" conf = ConfigParser.ConfigParser() defaults = { "captcha": { "ssl": "False", "public_key": "", "private_key": "", }, "email": { "host": "localhost", "port": "25", "username": "", "password": "", "recipients": "", "sender": "no-reply@%s" % socket.gethostname(), }, "logging": { "file": "/var/log/pyzor/web.log", "level": "INFO", "sentry": "", "sentry_level": "WARNING", } } # Load in default values. for section, values in defaults.iteritems(): conf.add_section(section) for option, value in values.iteritems(): conf.set(section, option, value) if os.path.exists("/etc/pyzor/web.conf"): # Overwrite with local values. conf.read("/etc/pyzor/web.conf") return conf def setup_logging(): logger = app.logger file_handler = logging.FileHandler(CONF.get("logging", "file")) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s %(message)s')) log_level = getattr(logging, CONF.get("logging", "level")) logger.setLevel(log_level) logger.addHandler(file_handler) raven_dsn = CONF.get("logging", "sentry") if raven_dsn: raven_log_level = getattr(logging, CONF.get("logging", "sentry_level")) sentry_handler = SentryHandler(raven_dsn) sentry_handler.setLevel(raven_log_level) logger.addHandler(sentry_handler) app = flask.Flask(__name__) CONF = load_configuration() SENTRY_DSN = CONF.get("logging", "sentry") setup_logging() app.config.update({ "RECAPTCHA_USE_SSL": CONF.get("captcha", "ssl").lower() == "true", "RECAPTCHA_PUBLIC_KEY": CONF.get("captcha", "public_key"), "RECAPTCHA_PRIVATE_KEY": CONF.get("captcha", "private_key"), }) if SENTRY_DSN: sentry = Sentry(app, dsn=SENTRY_DSN) class MessageForm(Form): digest = TextField("Pyzor digest*", validators=[length(40, 40, "Invalid Digest"), required()]) message = FileField('Raw message*') name = TextField('Name') email = EmailField('Email') comment = TextAreaField('Other details') recaptcha = RecaptchaField() submit = SubmitField() def __init___(self, *args, **kwargs): super(MessageForm, self).__init__(*args, **kwargs) self.msg = None self.raw_message = None self.logger = app.logger def validate(self): if not Form.validate(self): return False self.raw_message = flask.request.files["message"].stream.read() try: digest = pyzor.digest.DataDigester( email.message_from_string(self.raw_message)).value if digest != self.digest.data: self.add_error("digest", "Digest does not match message.") return False client = pyzor.client.Client(timeout=20) try: response = client.check(digest) except pyzor.TimeoutError as e: self.add_error("message", "Temporary error please try again.") self.logger.warn("Timeout: %s", e) return False except pyzor.CommError as e: self.add_error("message", "Temporary error please try again.") self.logger.warn("Error: %s", e) return False if not response.is_ok(): self.add_error("message", "Temporary error please try again.") self.logger.warn("Invalid response from server: %s", response) return False if int(response["Count"]) == 0: self.add_error("message", "Message not reported as spam.") return False if int(response["WL-Count"]) != 0: self.add_error("message", "Message is already whitelisted.") return False except AssertionError: self.add_error("message", "Invalid message.") return False return True def add_error(self, field, message): try: self.errors[field].append(message) except (KeyError, TypeError): self.errors[field] = [message] class WhitelistMessage(MethodView): def __init__(self): self.form = MessageForm(flask.request.form, csrf_enabled=False) self.logger = app.logger def get(self): return flask.render_template('whitelist.html', form=self.form, error=None) def post(self): success = False if self.form.validate(): msg = self.build_notification() self.send_email(msg) success = True return flask.render_template('whitelist.html', form=self.form, success=success) def build_notification(self): data = {"name": self.form.name.data, "email": self.form.email.data, "digest": self.form.digest.data, "comment": self.form.comment.data, "ip": flask.request.remote_addr} msg = email.mime.multipart.MIMEMultipart() msg["Date"] = email.utils.formatdate(localtime=True) msg["Subject"] = "[Pyzor] Whitelist request" msg["From"] = CONF.get("email", "sender") msg["To"] = CONF.get("email", "recipients") msg.preamble = "This is a multi-part message in MIME format." msg.epilogue = "" msg.attach(email.mime.text.MIMEText(MSG_TEMPLATE_TXT % data)) original_attachment = email.mime.base.MIMEBase("message", "rfc822") original_attachment.add_header("Content-Disposition", "attachment") original_attachment.set_payload(self.form.raw_message) msg.attach(original_attachment) return msg def send_email(self, msg): smtp = smtplib.SMTP(host=CONF.get("email", "host"), port=CONF.get("email", "port")) smtp.ehlo() try: code, err = smtp.mail(CONF.get("email", "sender")) if code != 250: raise smtplib.SMTPSenderRefused(code, err, CONF.get("email", "sender")) rcpterrs = {} for rcpt in CONF.get("email", "recipients").split(","): code, err = smtp.rcpt(rcpt) if code not in (250, 251): rcpterrs[rcpt] = (code, err) if rcpterrs: raise smtplib.SMTPRecipientsRefused(rcpterrs) code, err = smtp.data(msg.as_string()) if code != 250: raise smtplib.SMTPDataError(code, err) finally: try: smtp.quit() except smtplib.SMTPServerDisconnected: pass app.add_url_rule("/whitelist/", view_func=WhitelistMessage.as_view("whitelist")) @app.errorhandler(500) def unhandled_exception(error): """Generic error message.""" setup_logging() app.logger.error("Unhandled Exception: %s", error, exc_info=True) return flask.render_template('error.html', error=error) if __name__ == '__main__': app.debug = True app.run() pyzor-release-1-0-0/web/requirements.txt000066400000000000000000000000711244204744500203540ustar00rootroot00000000000000flask==0.10.1 flask-wtf==0.9.5 raven==5.0.0 pyzor==0.8.0 pyzor-release-1-0-0/web/static/000077500000000000000000000000001244204744500163615ustar00rootroot00000000000000pyzor-release-1-0-0/web/static/pyzor.gif000066400000000000000000000115031244204744500202330ustar00rootroot00000000000000GIF89a,ܙVˬUFxiĪŦd-3еd}۷ɂɎ7jxQwסLǫyϨCsw鳳o7jxSQĽ觾Ӯ?7j͙ˆɀћ࡚tŲn˳xw*̾歬:ռ䭬©7jټµ7ja`Ɣcڴyx󡡤TRӰͺ֊áykiϼo렟ˢjݹ]\법7jRwᄛǞѯ_<:ǀ߲𺐏n7j]כⷷї?nʎ֘s:X߱`ȔɖߕܹϹǹȬֺݹ߼neDpӨۦqȐ״7j!,, H*\Ȱ*QcË3jQ?4`Ɋ:\ɲe>%<_HTҥϟ@3@VAU U`C]2Co4x^HT۷p6-̷s25b ̧C>8a G`ɗ!C'h~T1s~L@cӨQWB#Ϻ+ШbڅgcA ރf:RΑ"33tOړ 2N>łK0*PL˟O?%d|&(aUR` 6`:| U3N=0  T< D| 5,0T LDIn#شw(n0K%> WDd(dPaihy BppܖM$ 9A"Q1 19i6`Cd_A> %t駞ÐjD <& @Oa\0+{A {05d!5tDg7p 3$0Ůd@M泥.<14@ʂEP *8 wĔ 1jZ# P<#dV l8aq%T%N3 ,lG7%h~Ե~}@ CCG0B@7bpֆMd:ubaBg($h.p B p< ,p)#8!n3Xa!Uo$L? )5PmH,>ZEB`o"#X6L b.qRÏ~+(@%p ArpDUrH-a8ьM؁)h 1YZܰ4pЫ s%jL$.*A; "0IF57Ml c fgAc;0F̡(- )P !CQBAL1@ p 8!# :ƀH8@`G QD}$3Xj6[crx0<eJaIB)L gx[*]@G  7xGwL-V*?Sc(Fłzna\R!BPYsdZ;Oو`hPc-,cXB zaG= 0ԁ%!ԡD( PۂmuK\.؈,9a Q@Vj0 `1B;j~H2;k*[ORX=8hBЂD^B7 Qnlz 0ѱZBkCpғ\4Lmʁ#`"h4NRp`ed*egN!mÇ > 1pB ~$e6pDa^C Bx 3*0=\$цjdĨ̈#d5p%15I;bH@T|7v9obTP V/FPuүL yPz`0T 0 y ` `W=9 mS= j!R@ɟ~ :0&AS5{p p0@50p0P 0E y=` tpr^#* xt P Q,ZfkwY Ԡ6zpd P0i0 b yPZ7"D j/ PR`@ 0@+p'`K e}ڟ| fP??p `|@P y0"7mp Q_6ٟ0T/`b ;4+p 4tFQ _` 09ô O6pAp k!*x5 (p #9,4<  NH_* Yl~nAA[s+5+ő?@ B`ܟZs4'n|Ȃ È | k1' fL;pyzor-release-1-0-0/web/static/style.css000066400000000000000000000010301244204744500202250ustar00rootroot00000000000000#header img { } #body_wrapper { margin: auto; width: 700px; } #content { padding: 10px; } .description { text-align: justify; } .form_wrapper { margin: 10px; } label { display: block; } input { width: 250px; margin: 10px 0px 10px 0px } textarea { height: 130px; width: 435px; margin: 10px 0px 10px 0px } #submit { width: 70px; } div.error{ display: inline; color: red; margin-left: 10px; } #whitelist_form label{ display: block; } #whitelist_form input{ } #success_message { color: green; }pyzor-release-1-0-0/web/templates/000077500000000000000000000000001244204744500170705ustar00rootroot00000000000000pyzor-release-1-0-0/web/templates/base.html000066400000000000000000000011331244204744500206660ustar00rootroot00000000000000 {% block head %} {% block title %}{% endblock %} - Pyzor {% endblock %}

{% block content %}{% endblock %}
pyzor-release-1-0-0/web/templates/error.html000066400000000000000000000003411244204744500211050ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Whitelist{% endblock %} {% block header %} {% endblock %} {% block content %}

Error

Internal error, please try again later.
{% endblock %} pyzor-release-1-0-0/web/templates/whitelist.html000066400000000000000000000032671244204744500220020ustar00rootroot00000000000000{% extends "base.html" %} {% macro error(name) -%}
{% if name in form.errors %} {{ form.errors[name][0] }} {% endif %}
{%- endmacro %} {% block title %}Whitelist{% endblock %} {% block header %} {% endblock %} {% block content %}

Whitelist

Please use this form to request whitelisting a message on public.pyzor.org. Please note that these message are not automatically updated, and they are first verified manually. For more information about pyzor visit pyzor.org.

In order to submit the request you will need to upload the original message and enter the pyzor digest of the message. See documentation for more details.


{{ form.digest.label }}{{ form.digest }}{{ error('digest') }} {{ form.message.label }}{{ form.message }}{{ error('message') }} {{ form.name.label }}{{ form.name }}{{ error('name') }} {{ form.email.label }}{{ form.email }}{{ error('email') }} {{ form.comment.label }}{{ form.comment }}{{ error('comment') }} {{ form.recaptcha }}
{{ error('recaptcha') }}
{% if success %} Request was successfully submitted. {% endif %}
{{ form.submit }}
{% endblock %}