pyzor-0.5.0/0000755000076500000240000000000011176137366012243 5ustar tameyerstaffpyzor-0.5.0/COPYING0000644000076500000240000004312711176137136013300 0ustar tameyerstaff 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-0.5.0/docs/0000755000076500000240000000000011176137366013173 5ustar tameyerstaffpyzor-0.5.0/docs/._pyzor.10000644000076500000240000000062311176137136014651 0ustar tameyerstaffMac OS X  2a“ATTR¢ö¹“¨ë¨ë%com.apple.metadata:kMDItemWhereFromsbplist00¢_chttps://sourceforge.net/tracker2/download.php?group_id=50000&atid=458244&file_id=308681&aid=2499743_Thttps://sourceforge.net/tracker2/?func=detail&aid=2499743&group_id=50000&atid=458244 qÈpyzor-0.5.0/docs/pyzor.10000644000076500000240000001060311176137136014433 0ustar tameyerstaff.TH PYZOR 1 "10 Oct 2002" .SH NAME pyzor \- spam\-catcher using a collaborative filtering network .SH SYNOPSIS \fBpyzor\fP [\fB\-d\fP] [\fB\-\-homedir\fP \fIdir\fP] \fIcommand\fP [\fIcommand\_options\fP] .SH OPTIONS .TP \fB\-d\fP turn on debugging .TP \fB\-\-homedir\fI dir\fP use dir as the home directory for Pyzor instead of the default ~/.pyzor. See the files section for more information on what files are inside of the homedir. .SH COMMANDS .TP \fBcheck\fP[\fB\-\-mbox\fP] Reads on standard input an RFC 822 (email) message. Exit code is zero (0) if and only if a match is found and the global whitelist count is zero. .BR If \-\-mbox is provided, then the input is assumed to be a unix mailbox, and all messages in it will be checked. .BR If multiple servers are listed in the configuration file, the exit code will be zero (0) if and only if there is a match found on at least one server (without it being whitelisted anyplace). .TP \fBreport \fP[\fB\-\-mbox\fP] Reads on standard input an RFC 822 (email) message. Reports to the server a digest of each message in the mailbox as spam. Writes to standard output a tuple of (error\-code, message) from the server. .BR If \-\-mbox is provided, then the input is assumed to be a unix mailbox, and all messages in it will be sent to the server. .TP \fBwhitelist \fP[\fB\-\-mbox\fP] Reads on standard input an RFC 822 (email) message. Sends to the server a digest of each message in the mailbox for whitelisting. Writes to standard output a tuple of (error\-code, message) from the server. .BR If \-\-mbox is provided, then the input is assumed to be a unix mailbox, and all messages in it will be sent to the server. .TP \fBdiscover\fP Finds Pyzor servers, and writes them to ~/.pyzor/servers. This may accomplished through querying already-known servers or an HTTP call to a hard-coded address. .TP \fBping\fP Merely requests a response from the servers. .TP \fBgenkey\fP Based upon a secret passphrase gathered from the user and salt gathered from /dev/random, prints to standard output a tuple of "salt,key". Used to put account information into the accounts file. See the section Using Accounts for more information. .TP \fBdigest \fP[\fB\-\-mbox\fP] Reads on standard input an RFC 822 (email) message. Writes the digest of the message to standard output. .BR If \-\-mbox is provided, then the input is assumed to be a unix mailbox, each message's digest is written to standard output, separated by newlines. .TP \fBpredigest\fP Reads on standard input an RFC 822 (email) message. Writes to standard output the normalized lines of data that are digested, with the exception that the lines printed have newlines (all whitespace is removed before digesting). .SH USING PYZOR WITH READYEXEC \fBReadyExec\fP is a system to eliminate the high startup-cost of executing scripts repeatedly. If you execute pyzor a lot, you might be interested in installing ReadyExec and using it with pyzor. To use pyzor with ReadyExec, the readyexecd.py server needs to be started as: readyexecd.py socket_file pyzor socket_file can be any (non\-existing) filename you wish ReadyExec to use, such as /tmp/pyzor: readyexecd.py /tmp/pyzor pyzor Individual clients are then executed as: readyexec socket_file options command cmd_options For example: readyexec /tmp/pyzor check readyexec /tmp/pyzor report readyexec /tmp/pyzor whitelist \-\-mbox readyexec /tmp/pyzor \-d ping ReadyExec can be found at: http://readyexec.sourceforge.net/ .SH INTEGRATION WITH MUTT Add the following line to mutt.conf: macro index S "|/usr/bin/pyzor report" Then press S on the spam message in mutt to report it with pyzor. .SH FILES \fI~/.pyzor/config\fP The format of this file is INI-style (name=value, divided into [sections]). Names are case insensitive. All values which are filenames can have shell\-style tildes (~) in them. All values which are relative filenames are interpreted to be relative to the Pyzor homedir. \fBDefaults\fP [client] ServersFile = servers AccountsFile = accounts DiscoverServersURL = http://pyzor.sourceforge.net/cgi-bin/inform\-servers\-0\-3\-x Timeout = 5 .SH SEE ALSO pyzord(1) .SH AUTHOR This manpage was originally written by Bastian Kleineidam for the Debian distribution of pyzor but may be used by others. .BR The main author of pyzor is Frank J. Tobin . The main project page for pyzor can be found at http://sourceforge.net/projects/pyzor pyzor-0.5.0/docs/._pyzord.10000644000076500000240000000062311176137136015015 0ustar tameyerstaffMac OS X  2a“ATTR¢öº“¨ë¨ë%com.apple.metadata:kMDItemWhereFromsbplist00¢_chttps://sourceforge.net/tracker2/download.php?group_id=50000&atid=458244&file_id=308682&aid=2499743_Thttps://sourceforge.net/tracker2/?func=detail&aid=2499743&group_id=50000&atid=458244 qÈpyzor-0.5.0/docs/pyzord.10000644000076500000240000000234411176137136014602 0ustar tameyerstaff.TH PYZORD 1 "10 Oct 2002" .SH NAME pyzord \- spam\-catching server .SH SYNOPSIS \fBpyzord\fP [\fB\-d\fP] [\fB\-\-homedir\fP \fIdir\fP] Note: pyzord does not daemonize itself. Note: logging information is written to standard output. .SH OPTIONS .TP \fB\-d\fP Turn on debugging .TP \fB\-\-homedir\fP \fIdir\fP use dir as the home directory for Pyzor instead of the default ~/.pyzor. See the files section for more information on what files are inside of the homedir. .SH FILES \fI~/.pyzor/config\fP The format of this file is INI-style (name=value, divided into [sections]). Names are case insensitive. All values which are filenames can have shell\-style tildes (~) in them. All values which are relative filenames are interpreted to be relative to the Pyzor homedir. \fBDefaults\fP [server] Port = 24441 ListenAddress = 0.0.0.0 DigestDB = pyzord.db PasswdFile = pyzord.passwd AccessFile = pyzord.access .SH SEE ALSO pyzor(1) .SH AUTHOR This manpage was originally written by Bastian Kleineidam for the Debian distribution of pyzor but may be used by others. .BR The main author of pyzor is Frank J. Tobin . The main project page for pyzor can be found at http://sourceforge.net/projects/pyzor pyzor-0.5.0/docs/usage.html0000644000076500000240000003502511176137136015165 0ustar tameyerstaff Pyzor Usage Documentation

Pyzor Usage Documentation

$Id: usage.html,v 1.11 2002-10-10 21:37:08 ftobin Exp $

pyzor (client)

pyzor [-d] [--homedir dir] command [command_options]
    

options

-d
turn on debugging
--homedir dir
use dir as the home directory for Pyzor instead of the default ~/.pyzor. See the files section for more information on what files are inside of the homedir.

commands

General Output

In general, the output from pyzor command is of the form:

ip:port (response-code, response-text) command-specific-output

Note that these are separated by tabs.

check

Reads on standard input an RFC 822 (email) message. Exit code is zero (0) if and only if a match is found and the global whitelist count is zero.

If multiple servers are listed in the configuration file, the exit code will be zero (0) if and only if there is a match found on at least one server (without it being whitelisted anyplace).

The command-specfic output for a check is report-count whitelist-count.

report [--mbox]

Reads on standard input an RFC 822 (email) message. Reports to the server a digest of each message in the mailbox as spam. Writes to standard output a tuple of (error-code, message) from the server.

If --mbox is provided, then the input is assumed to be a unix mailbox, and all messages in it will be sent to the server.

whitelist [--mbox]

Reads on standard input an RFC 822 (email) message. Sends to the server a digest of each message in the mailbox for whitelisting. Writes to standard output a tuple of (error-code, message) from the server.

If --mbox is provided, then the input is assumed to be a unix mailbox, and all messages in it will be sent to the server.

discover

Finds Pyzor servers, and writes them to ~/.pyzor/servers. This may accomplished through querying already-known servers or an HTTP call to a hard-coded address.

ping

Merely requests a response from the servers.

genkey
Based upon a secret passphrase gathered from the user and salt gathered from /dev/random, prints to standard output a tuple of "salt,key". Used to put account information into the accounts file. See the section Using Accounts for more information.
digest [--mbox]

Reads on standard input an RFC 822 (email) message. Writes the digest of the message to standard output.

If --mbox is provided, then the input is assumed to be a unix mailbox, each message's digest is written to standard output, separated by newlines.

predigest

Reads on standard input an RFC 822 (email) message. Writes to standard output the normalized lines of data that are digested, with the exception that the lines printed have newlines (all whitespace is removed before digesting).

Using the Pyzor client with procmail

To use Pyzor in a procmail system, consider using the following simple recipe.

:0 Wc
| pyzor check
:0 a
pyzor-caught
    

If you prefer, you can merely add a header to message marked with Pyzor, instead of immediately filtering them into a separate folder:

:0 Wc
| pyzor check
:0 Waf
| formail -A 'X-Pyzor: spam'
    

Using the Pyzor client with ReadyExec

ReadyExec is a system to eliminate the high startup-cost of executing scripts repeatedly. If you execute pyzor a lot, you might be interested in installing ReadyExec and using it with pyzor.

To use pyzor with ReadyExec, the readyexecd.py server needs to be started as:

readyexecd.py socket_file pyzor.client.run
    

socket_file can be any (non-existing) filename you wish ReadyExec to use, such as /tmp/pyzor:

readyexecd.py /tmp/pyzor pyzor.client.run
    

Individual clients are then executed as:

readyexec socket_file options command cmd_options
    

For example:

readyexec /tmp/pyzor check
readyexec /tmp/pyzor report
readyexec /tmp/pyzor whitelist --mbox
readyexec /tmp/pyzor -d ping
    

pyzord (server)

pyzord [-d] [--homedir dir]
    

Note: pyzord does not daemonize itself.

Note: logging information is written to standard output.

options

-d
Turn on debugging (writes information to standard error)
--homedir dir
use dir as the home directory for Pyzor instead of the default ~/.pyzor. See the files section for more information on what files are inside of the homedir.

Files

~/pyzor/config

The format of this file is INI-style (name=value, divided into [sections]). Names are case insensitive. All values which are filenames can have shell-style tildes (~) in them. All values which are relative filenames are interpreted to be relative to the Pyzor homedir.

Defaults

[client]
ServersFile = servers
AccountsFile = accounts
DiscoverServersURL = http://pyzor.sourceforge.net/cgi-bin/inform-servers-0-3-x
Timeout = 5

[server]
Port = 24441
ListenAddress = 0.0.0.0
DigestDB   = pyzord.db
PasswdFile = pyzord.passwd
AccessFile = pyzord.access
    

Definitions

[client] section
ServersFile
A newline-separated list of server addresses to report/whitelist/check with. Addresses are in the format host:port. Can be populated with pyzor discover.
AccountsFile

File containing information about accounts on servers. Format is line-oriented, with each line being:

host : port : username : salt,key
	

Example:

127.0.0.1 : 9999 : bob : 227bfb58efaba7c582d9dcb66ab2063d38df2923,8da9f54058c34e383e997f45d6eb74837139f83b
	

See the section Using Accounts for more information.

DiscoverServersURL
During pyzor discover, the URL to use in finding servers.
Timeout
Number of seconds to wait for response to a query.
[server] section
Port
port to listen on
ListenAddress
address to listen on
DigestDB
file containing the database of digests
PasswdFile

File containing a list of user account information. Line format:

username : key
	

Example:

bob : 8da9f54058c34e383e997f45d6eb74837139f83b
	
AccessFile

File containing information about user privileges. The format is very similar to the popular tcp_wrappers hosts.{allow,deny}:

privilege ... : username ... : allow|deny
	
privilege ...
a list of whitespace-separated commands such as report, check, and whitelist, generally corresponding to the Pyzor client commands. The keyword all can be used to to refer to all commands.
username ...
a list of whitespace-separated usernames. The keyword all can be used to refer to all users. The anonymous user is refereed to as anonymous.
allow|deny
Whether or not the specified user(s) can perform the specified privilege(s) on the line.

The file is processed from top to bottom, with the first match for user/privilege being the value taken. Every file has the following implicit final rule:

all : all : deny
	

If this file is nonexistant, the following default is used:

check report ping info : anonymous : allow
	

Using Accounts

To get an account on a server requires coordination between the client user and server admin. Use the following steps:

  1. 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'.
  2. User generates a key with pyzor genkey, and puts an entry into their ~/.pyzor/accounts. 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
    	
  3. The user then sends the key (the part to the right-hand side of the comma) to the admin.
  4. The admin adds the following to their ~/.pyzor/pyzord.passwd:

    bob : 8da9f54058c34e383e997f45d6eb74837139f83b
    	
  5. 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 info whitelist : bob : allow
    	
  6. The server needs to be restarted in order for this to take effect.

User-level Differences from Razor

This is a small list of user-recognizable differences between Pyzor and Razor that one might notice if coming from Razor to Pyzor.

  • Pyzor does not consult a whitelist system for you before checking whether to send a message. This is best handled by other systems, such as other procmail rules.
  • Pyzor does not currently implement a web-of-trust system.

Copyright © 2002-9 Frank J. Tobin ftobin@neverending.org

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, visit the following URL: http://www.gnu.org/copyleft/gpl.html.

pyzor-0.5.0/INSTALL0000644000076500000240000000237411176137136013275 0ustar tameyerstaffPyzor requires at least Python 2.2.1 To install this distribution, simply run the following: python setup.py build python setup.py install Note that your system might install the modules and scripts with non-world-readable permissions. Correct this with a command such as: chmod -R a+rX /usr/share/doc/pyzor \ /usr/lib/python2.2/site-packages/pyzor \ /usr/bin/pyzor /usr/bin/pyzord To use the server, the Python gdbm module is required. You can generally check if you have the gdbm module by executing: python -c 'import gdbm' && echo 'gdbm found' The gdbm module is available at: Debian GNU/Linux: http://packages.debian.org/stable/interpreters/python-gdbm.html Gentoo Linux: Will be built with Python if the gdbm library is found. If it isn't there with your Python, try stealing the FreeBSD setup.py patchfile in their ports to install just the gdbm module, or simply re-install Python. FreeBSD: ports/databases/py-gdbm tar.gz: included in the Python distribution (not sure of the precise procedure for simply installing the gdbm module; try stealing the FreeBSD setup.py patchfile in their ports). See docs/usage.html for usage documentation, and if you are upgrading from another version of Pyzor, please read the UPGRADING file. pyzor-0.5.0/lib/0000755000076500000240000000000011176137366013011 5ustar tameyerstaffpyzor-0.5.0/lib/pyzor/0000755000076500000240000000000011176137366014174 5ustar tameyerstaffpyzor-0.5.0/lib/pyzor/__init__.py0000644000076500000240000002777711176137136016324 0ustar tameyerstaff"""networked spam-signature detection""" __author__ = "Frank J. Tobin, ftobin@neverending.org" __version__ = "0.5.0" __revision__ = "$Id: __init__.py,v 1.43 2002-09-17 15:12:58 ftobin Exp $" import os import os.path import re import sys import sha import tempfile import random import ConfigParser import rfc822 import cStringIO import time proto_name = 'pyzor' proto_version = 2.0 class CommError(Exception): """Something in general went wrong with the transaction""" pass class ProtocolError(CommError): """Something is wrong with talking the protocol""" pass class TimeoutError(CommError): pass class IncompleteMessageError(ProtocolError): pass class UnsupportedVersionError(ProtocolError): pass class SignatureError(CommError): """unknown user, signature on msg invalid, or not within allowed time range""" pass class Singleton(object): __slots__ = [] def __new__(cls, *args, **kwds): it = cls.__dict__.get('__it__') if it is None: cls.__it__ = object.__new__(cls) return cls.__it__ class BasicIterator(object): def __iter__(self): return self def next(self): raise NotImplementedError class Username(str): user_pattern = re.compile(r'^[-\.\w]+$') def __init__(self, s): self.validate() def validate(self): if not self.user_pattern.match(self): raise ValueError, "%s is an invalid username" % self class Opname(str): op_pattern = re.compile(r'^[-\.\w]+$') def __init__(self, s): self.validate() def validate(self): if not self.op_pattern.match(self): raise ValueError, "%s is an invalid username" % self class Output(Singleton): do_debug = False quiet = False def __init__(self, quiet=None, debug=None): if quiet is not None: self.quiet = quiet if debug is not None: self.do_debug = debug def data(self, msg): print msg def warn(self, msg): if not self.quiet: sys.stderr.write('%s\n' % msg) def debug(self, msg): if self.do_debug: sys.stderr.write('%s\n' % msg) class DataDigest(str): # hex output doubles digest size value_size = sha.digest_size * 2 def __init__(self, value): if len(value) != self.value_size: raise ValueError, "invalid digest value size" class DataDigestSpec(list): """a list of tuples, (perc_offset, length)""" def validate(self): for t in self: self.validate_tuple(t) def validate_tuple(t): (perc_offset, length) = t if not (0 <= perc_offset < 100): raise ValueError, "offset percentage out of bounds" if not length > 0: raise ValueError, "piece lengths must be positive" validate_tuple = staticmethod(validate_tuple) def netstring(self): # flattened, commified return ','.join(map(str, reduce(lambda x, y: x + y, self, ()))) def from_netstring(self, s): new_spec = apply(self) expanded_list = s.split(',') if len(extended_list) % 2 != 0: raise ValueError, "invalid list parity" for i in range(0, len(expanded_list), 2): perc_offset = int(expanded_list[i]) length = int(expanded_list[i+1]) self.validate_tuple(perc_offset, length) new_spec.append((perc_offset, length)) return new_spec from_netstring = classmethod(from_netstring) class Message(rfc822.Message, object): def __init__(self, fp=None): if fp is None: fp = cStringIO.StringIO() super(Message, self).__init__(fp) self.setup() def setup(self): """called after __init__, designed to be extended""" pass def init_for_sending(self): if __debug__: self.ensure_complete() def __str__(self): s = ''.join(self.headers) s += '\n' self.rewindbody() # okay to slurp since we're dealing with UDP s += self.fp.read() return s def __nonzero__(self): # just to make sure some old code doesn't try to use this raise NotImplementedError def ensure_complete(self): pass class ThreadedMessage(Message): def init_for_sending(self): if not self.has_key('Thread'): self.set_thread(ThreadId.generate()) assert self.has_key('Thread') self.setdefault('PV', str(proto_version)) super(ThreadedMessage, self).init_for_sending() def ensure_complete(self): if not (self.has_key('PV') and self.has_key('Thread')): raise IncompleteMessageError, \ "doesn't have fields for a ThreadedMessage" super(ThreadedMessage, self).ensure_complete() def get_protocol_version(self): return float(self['PV']) def get_thread(self): return ThreadId(self['Thread']) def set_thread(self, i): typecheck(i, ThreadId) self['Thread'] = str(i) class MacEnvelope(Message): ts_diff_max = 300 def ensure_complete(self): if not (self.has_key('User') and self.has_key('Time') and self.has_key('Sig')): raise IncompleteMessageError, \ "doesn't have fields for a MacEnvelope" super(MacEnvelope, self).ensure_complete() def get_submsg(self, factory=ThreadedMessage): self.rewindbody() return apply(factory, (self.fp,)) def verify_sig(self, user_key): typecheck(user_key, long) user = Username(self['User']) ts = int(self['Time']) said_sig = self['Sig'] hashed_user_key = self.hash_key(user_key, user) if abs(time.time() - ts) > self.ts_diff_max: raise SignatureError, "timestamp not within allowed range" msg = self.get_submsg() calc_sig = self.sign_msg(hashed_user_key, ts, msg) if not (calc_sig == said_sig): raise SignatureError, "invalid signature" def wrap(self, user, key, msg): """This should be used to create a MacEnvelope""" typecheck(user, str) typecheck(msg, Message) typecheck(key, long) env = apply(self) ts = int(time.time()) env['User'] = user env['Time'] = str(ts) env['Sig'] = self.sign_msg(self.hash_key(key, user), ts, msg) env.fp.write(str(msg)) return env wrap = classmethod(wrap) def hash_msg(msg): """returns a digest object""" typecheck(msg, Message) return sha.new(str(msg)) hash_msg = staticmethod(hash_msg) def hash_key(key, user): """returns lower(H(U + ':' + lower(hex(K))))""" typecheck(key, long) typecheck(user, Username) return sha.new("%s:%x" % (Username, key)).hexdigest().lower() hash_key = staticmethod(hash_key) def sign_msg(self, hashed_key, ts, msg): """ts is timestamp for message (epoch seconds) S = H(H(M) + ':' T + ':' + K) M is message T is decimal epoch timestamp K is hashed_key returns a digest object""" typecheck(ts, int) typecheck(msg, Message) typecheck(hashed_key, str) h_msg = self.hash_msg(msg) return sha.new("%s:%d:%s" % (h_msg.digest(), ts, hashed_key)).hexdigest().lower() sign_msg = classmethod(sign_msg) class Response(ThreadedMessage): ok_code = 200 def ensure_complete(self): if not(self.has_key('Code') and self.has_key('Diag')): raise IncompleteMessageError, \ "doesn't have fields for a Response" super(Response, self).ensure_complete() def is_ok(self): return self.get_code() == self.ok_code def get_code(self): return int(self['Code']) def get_diag(self): return self['Diag'] def head_tuple(self): return (self.get_code(), self.get_diag()) class Request(ThreadedMessage): """this is class that should be used to read in Requests of any type. subclasses are responsible for setting 'Op' if they are generating a message""" def get_op(self): return self['Op'] def ensure_complete(self): if not self.has_key('Op'): raise IncompleteMessageError, \ "doesn't have fields for a Request" super(Request, self).ensure_complete() class ClientSideRequest(Request): def setup(self): super(Request, self).setup() self.setdefault('Op', self.op) class PingRequest(ClientSideRequest): op = Opname('ping') class ShutdownRequest(ClientSideRequest): op = Opname('shutdown') class SimpleDigestBasedRequest(ClientSideRequest): def __init__(self, digest): typecheck(digest, str) super(SimpleDigestBasedRequest, self).__init__() self.setdefault('Op-Digest', digest) class CheckRequest(SimpleDigestBasedRequest): op = Opname('check') class InfoRequest(SimpleDigestBasedRequest): op = Opname('info') class SimpleDigestSpecBasedRequest(SimpleDigestBasedRequest): def __init__(self, digest, spec): typecheck(digest, str) typecheck(spec, DataDigestSpec) super(SimpleDigestSpecBasedRequest, self).__init__(digest) self.setdefault('Op-Spec', spec.netstring()) class ReportRequest(SimpleDigestSpecBasedRequest): op = Opname('report') class WhitelistRequest(SimpleDigestSpecBasedRequest): op = Opname('whitelist') class ErrorResponse(Response): def __init__(self, code, s): typecheck(code, int) typecheck(s, str) super(ErrorResponse, self).__init__() self.setdefault('Code', str(code)) self.setdefault('Diag', s) class ThreadId(int): # (0, 1024) is reserved full_range = (0, 2**16) ok_range = (1024, full_range[1]) error_value = 0 def __init__(self, i): super(ThreadId, self).__init__(i) if not (self.full_range[0] <= self < self.full_range[1]): raise ValueError, "value outside of range" def generate(self): return apply(self, (apply(random.randrange, self.ok_range),)) generate = classmethod(generate) def in_ok_range(self): return (self >= self.ok_range[0] and self < self.ok_range[1]) class Address(tuple): def __init__(self, *varargs, **kwargs): self.validate() def validate(self): typecheck(self[0], str) typecheck(self[1], int) if len(self) != 2: raise ValueError, "invalid address: %s" % str(self) def __str__(self): return (self[0] + ':' + str(self[1])) def from_str(self, s): fields = s.split(':') fields[1] = int(fields[1]) return self(fields) from_str = classmethod(from_str) class Config(ConfigParser.ConfigParser, object): def __init__(self, homedir): assert isinstance(homedir, str) self.homedir = homedir super(Config, self).__init__() def get_filename(self, section, option): fn = os.path.expanduser(self.get(section, option)) if not os.path.isabs(fn): fn = os.path.join(self.homedir, fn) return fn def get_homedir(specified): homedir = os.path.join('/etc', 'pyzor') if specified is not None: homedir = specified else: userhome = os.getenv('HOME') if userhome is not None: homedir = os.path.join(userhome, '.pyzor') return homedir def typecheck(inst, type_): if not isinstance(inst, type_): raise TypeError def modglobal_apply(globs, repl, obj, varargs=(), kwargs=None): """temporarily modify globals during a call. globs is the globals to modify (e.g., the return from globals()) repl is a dictionary of name: value replacements for the global dict.""" if kwargs is None: kwargs = {} saved = {} for (k, v) in repl.items(): saved[k] = globs[k] globs[k] = v try: r = apply(obj, varargs, kwargs) finally: globs.update(saved) return r anonymous_user = Username('anonymous') pyzor-0.5.0/lib/pyzor/client.py0000644000076500000240000007420311176137136016025 0ustar tameyerstaff"""networked spam-signature detection client""" import re import os import os.path import socket import signal import cStringIO import getopt import tempfile import mimetools import multifile import sha import pyzor from pyzor import * __author__ = pyzor.__author__ __version__ = pyzor.__version__ __revision__ = "$Id: client.py,v 1.48 2003-02-01 10:29:42 ftobin Exp $" randfile = '/dev/random' class Client(object): __slots__ = ['socket', 'output', 'accounts'] timeout = 5 max_packet_size = 8192 def __init__(self, accounts): signal.signal(signal.SIGALRM, handle_timeout) self.accounts = accounts self.output = Output() self.build_socket() def ping(self, address): msg = PingRequest() self.send(msg, address) return self.read_response(msg.get_thread()) def info(self, digest, address): msg = InfoRequest(digest) self.send(msg, address) return self.read_response(msg.get_thread()) def report(self, digest, spec, address): msg = ReportRequest(digest, spec) self.send(msg, address) return self.read_response(msg.get_thread()) def whitelist(self, digest, spec, address): msg = WhitelistRequest(digest, spec) self.send(msg, address) return self.read_response(msg.get_thread()) def check(self, digest, address): msg = CheckRequest(digest) self.send(msg, address) return self.read_response(msg.get_thread()) def build_socket(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def send(self, msg, address): msg.init_for_sending() account = self.accounts[address] mac_msg_str = str(MacEnvelope.wrap(account.username, account.keystuff.key, msg)) self.output.debug("sending: %s" % repr(mac_msg_str)) self.socket.sendto(mac_msg_str, 0, address) def recv(self): return self.time_call(self.socket.recvfrom, (self.max_packet_size,)) def time_call(self, call, varargs=(), kwargs=None): if kwargs is None: kwargs = {} signal.alarm(self.timeout) try: return apply(call, varargs, kwargs) finally: signal.alarm(0) def read_response(self, expect_id): (packet, address) = self.recv() self.output.debug("received: %s" % repr(packet)) msg = Response(cStringIO.StringIO(packet)) msg.ensure_complete() try: thread_id = msg.get_thread() if thread_id != expect_id: if thread_id.in_ok_range(): raise ProtocolError, \ "received unexpected thread id %d (expected %d)" \ % (thread_id, expect_id) else: self.output.warn("received error thread id %d (expected %d)" % (thread_id, expect_id)) except KeyError: self.output.warn("no thread id received") return msg class ServerList(list): inform_url = 'http://pyzor.sourceforge.net/cgi-bin/inform-servers-0-3-x' def read(self, serverfile): for line in serverfile: orig_line = line line = line.strip() if line and not line.startswith('#') and \ re.match('[a-zA-Z0-9.-]+:[0-9]+', line): self.append(pyzor.Address.from_str(line)) class ExecCall(object): __slots__ = ['client', 'servers', 'output'] # hard-coded for the moment digest_spec = DataDigestSpec([(20, 3), (60, 3)]) def run(self): debug = 0 log = None options = None try: (options, args) = getopt.getopt(sys.argv[1:], 'dh:', ['homedir=', 'log', 'help']) except getopt.GetoptError: self.usage() if len(args) < 1: self.usage() specified_homedir = None for (o, v) in options: if o == '-d': debug = 1 elif o in ('-h', '--help'): self.usage() elif o == '--homedir': specified_homedir = v elif o == '--log': log = 1 self.output = Output(debug=debug) homedir = pyzor.get_homedir(specified_homedir) if log: sys.stderr = open(homedir + "/pyzor.log", 'a') sys.stderr.write("\npyzor[" + repr (os.getpid()) + "]:\n") config = pyzor.Config(homedir) config.add_section('client') defaults = {'ServersFile': 'servers', 'DiscoverServersURL': ServerList.inform_url, 'AccountsFile': 'accounts', 'Timeout': str(Client.timeout), } for k, v in defaults.items(): config.set('client', k, v) config.read(os.path.join(homedir, 'config')) servers_fn = config.get_filename('client', 'ServersFile') Client.timeout = config.getint('client', 'Timeout') if not os.path.exists(homedir): os.mkdir(homedir) command = args[0] if not os.path.exists(servers_fn) or command == 'discover': sys.stderr.write("downloading servers from %s\n" % config.get('client', 'DiscoverServersURL')) download(config.get('client', 'DiscoverServersURL'), servers_fn) self.servers = self.get_servers(servers_fn) if not len(self.servers): sys.stderr.write("no valid servers found\n") # Remove the servers file as it only contains invalid entries, # probably a HTTP error message. os.remove(servers_fn) sys.exit(1) self.client = Client(self.get_accounts(config.get_filename('client', 'AccountsFile'))) if not self.dispatches.has_key(command): self.usage() dispatch = self.dispatches[command] if dispatch is not None: try: if not apply(dispatch, (self, args)): sys.exit(1) except TimeoutError: # note that most of the methods will trap # their own timeout error sys.stderr.write("timeout from server\n") sys.exit(1) def usage(self, s=None): if s is not None: sys.stderr.write("%s\n" % s) sys.stderr.write(""" usage: %s [-d] [--homedir dir] command [cmd_opts] command is one of: check, report, discover, ping, digest, predigest, genkey Data is read on standard input (stdin). """ % sys.argv[0]) sys.exit(2) return # just to help xemacs def ping(self, args): try: getopt.getopt(args[1:], '') except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) runner = ClientRunner(self.client.ping) for server in self.servers: runner.run(server, (server,)) return runner.all_ok def info(self, args): try: (options, args2) = getopt.getopt(args[1:], '', ['mbox']) except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) do_mbox = 'msg' for (o, v) in options: if o == '--mbox': do_mbox = 'mbox' runner = InfoClientRunner(self.client.info) for digest in get_input_handler(sys.stdin, self.digest_spec, do_mbox): if not digest: continue for server in self.servers: response = runner.run(server, (digest, server)) return True def check(self, args): try: (options, args2) = getopt.getopt(args[1:], '', ['mbox']) except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) do_mbox = 'msg' for (o, v) in options: if o == '--mbox': do_mbox = 'mbox' runner = CheckClientRunner(self.client.check) for digest in get_input_handler(sys.stdin, self.digest_spec, do_mbox): if not digest: continue for server in self.servers: runner.run(server, (digest, server)) return (runner.found_hit and not runner.whitelisted) def report(self, args): try: (options, args2) = getopt.getopt(args[1:], '', ['mbox']) except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) do_mbox = 'msg' for (o, v) in options: if o == '--mbox': do_mbox = "mbox" all_ok = True for digest in get_input_handler(sys.stdin, self.digest_spec, do_mbox): if not digest: continue if not self.send_digest(digest, self.digest_spec, self.client.report): all_ok = False return all_ok def send_digest(self, digest, spec, client_method): """digest can be none; if so, nothing is sent""" if digest is None: return typecheck(digest, DataDigest) runner = ClientRunner(client_method) for server in self.servers: runner.run(server, (digest, spec, server)) return runner.all_ok def whitelist(self, args): try: (options, args2) = getopt.getopt(args[1:], '', ['mbox']) except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) do_mbox = "msg" for (o, v) in options: if o == '--mbox': do_mbox = "mbox" all_ok = True for digest in get_input_handler(sys.stdin, self.digest_spec, do_mbox): if not digest: continue if not self.send_digest(digest, self.digest_spec, self.client.whitelist): all_ok = False return all_ok def digest(self, args): try: (options, args2) = getopt.getopt(args[1:], '', ['mbox']) except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) do_mbox = "msg" for (o, v) in options: if o == '--mbox': do_mbox = "mbox" for digest in get_input_handler(sys.stdin, self.digest_spec, do_mbox): if not digest: continue sys.stdout.write("%s\n" % digest) return True def print_digested(self, args): try: getopt.getopt(args[1:], '') except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) do_mbox = "msg" def loop(): for digest in get_input_handler(sys.stdin, self.digest_spec, do_mbox): pass modglobal_apply(globals(), {'DataDigester': PrintingDataDigester}, loop) return True def genkey(self, args): try: getopt.getopt(args[1:], '') except getopt.GetoptError: self.usage("%s does not take any non-option arguments" % args[0]) import getpass p1 = getpass.getpass(prompt='Enter passphrase: ') p2 = getpass.getpass(prompt='Enter passphrase again: ') if p1 != p2: sys.stderr.write("Passwords do not match.\n") return 0 del p2 saltfile = open(randfile) salt = saltfile.read(sha.digest_size) del saltfile salt_digest = sha.new(salt) pass_digest = sha.new() pass_digest.update(salt_digest.digest()) pass_digest.update(p1) sys.stdout.write("salt,key:\n") sys.stdout.write("%s,%s\n" % (salt_digest.hexdigest(), pass_digest.hexdigest())) return True def get_servers(servers_fn): servers = ServerList() servers.read(open(servers_fn)) if len(servers) == 0: sys.stderr.write("No servers available! Maybe try the 'discover' command\n") sys.exit(1) return servers get_servers = staticmethod(get_servers) def get_accounts(accounts_fn): accounts = AccountsDict() if os.path.exists(accounts_fn): for address, account in AccountsFile(open(accounts_fn)): accounts[address] = account return accounts get_accounts = staticmethod(get_accounts) dispatches = {'check': check, 'report': report, 'ping' : ping, 'genkey': genkey, 'info': info, 'whitelist': whitelist, 'digest': digest, 'predigest': print_digested, 'discover': None, # handled earlier } class DataDigester(object): """The major workhouse class""" __slots__ = ['_atomic', '_value', '_used_line', '_digest', 'seekable'] # minimum line length for it to be included as part # of the digest. I forget the purpose, however. # Someone remind me so I can document it here. min_line_length = 8 # if a message is this many lines or less, then # we digest the whole message atomic_num_lines = 4 # We're not going to try to match email addresses # as per the spec because it's too freakin difficult # Plus, regular expressions don't work well for them. # (BNF is better at balanced parens and such) email_ptrn = re.compile(r'\S+@\S+') # same goes for URL's url_ptrn = re.compile(r'[a-z]+:\S+', re.IGNORECASE) # We also want to remove anything that is so long it # looks like possibly a unique identifier longstr_ptrn = re.compile(r'\S{10,}') html_tag_ptrn = re.compile(r'<.*?>') ws_ptrn = re.compile(r'\s') # we might want to change this in the future. # Note that an empty string will always be used to remove whitespace unwanted_txt_repl = '' def __init__(self, fp, spec, seekable=True): self._atomic = None self._value = None self._used_line = None self.seekable = seekable (fp, offsets) = self.get_line_offsets(fp) # did we get an empty (parsed output) file? if len(offsets) == 0: return self._digest = sha.new() if len(offsets) <= self.atomic_num_lines: self.handle_atomic(fp) else: self.handle_pieced(fp, spec, offsets) self._value = DataDigest(self._digest.hexdigest()) assert self._atomic is not None assert self._value is not None def handle_atomic(self, fp): """we digest everything""" self._atomic = True try: fp.seek(0) for line in fp: self.handle_line(line) except: pass def handle_pieced(self, fp, spec, offsets): self._atomic = False """digest stuff according to the spec""" for (perc_offset, length) in spec: assert 0 <= perc_offset < 100 offset = offsets[int(perc_offset * len(offsets) / 100.0)] fp.seek(offset) for i in range(length): line = fp.readline() if not line: break self.handle_line(line) def get_line_offsets(self, fp): """return tuple of (fp2, line offsets) If we are not seekable, fp will be copied into a tempfile, and fp2 is hence re-usable. If we are not seekable, we also normalize the lines while copying them into the tempfile. """ if self.seekable: cur_offset = fp.tell() newfp = None else: # we need a seekable file to make # line-based skipping around to be more efficient # than loading the whole thing into memory cur_offset = 0 newfp = tempfile.TemporaryFile() offsets = [] for line in fp: norm = self.normalize(line) should_handle = self.should_handle_line(norm) if should_handle: offsets.append(cur_offset) # the thing to remember about cur_offset is that it should # be used to specify where to seek to in the # 'output' document, not where we currently are in fp # Remember, the output document is static if we are seekable # (because we don't have to write out a tempfile), # but it's *not the same* if we're writing out a new document, # since we don't need to write out all the lines. moved = 0 if self.seekable: moved = len(line) elif should_handle: norm += "\n" moved = len(norm) newfp.write(norm) cur_offset += moved if not self.seekable: fp = newfp return (fp, offsets) def handle_line(self, line): # seekable indicates that # the line was not normalized # when we first ran over it to get the line offsets if self.seekable: buf = self.normalize(line) else: # we at least have to strip the newline buf = line.rstrip() self._really_handle_buf(buf) def _really_handle_buf(self, buf): self._digest.update(buf) def is_atomic(self): if self._atomic is None: raise RuntimeError, "digest not calculated yet" return bool(self._atomic) def get_digest(self): return self._value def normalize(self, s): repl = self.unwanted_txt_repl s2 = s s2 = self.longstr_ptrn.sub(repl, s2) s2 = self.email_ptrn.sub(repl, s2) s2 = self.url_ptrn.sub(repl, s2) s2 = self.html_tag_ptrn.sub(repl, s2) # make sure we do the whitespace last because some of # the previous patterns rely on whitespace s2 = self.ws_ptrn.sub('', s2) return s2 normalize = classmethod(normalize) def should_handle_line(self, s): return bool(self.min_line_length <= len(s)) should_handle_line = classmethod(should_handle_line) class PrintingDataDigester(DataDigester): """extends DataDigester: prints out what we're digesting""" def _really_handle_buf(self, buf): sys.stdout.write("%s\n" % buf) super(PrintingDataDigester, self)._really_handle_buf(buf) def get_input_handler(fp, spec, style='msg', seekable=False): """Return an object that can be iterated over to get all the digests from fp according to spec. mbox is a boolean""" if style == 'msg': return filter(lambda x: x is not None, (DataDigester(rfc822BodyCleaner(fp), spec, seekable).get_digest(),) ) elif style =='mbox': return MailboxDigester(fp, spec) elif style == 'digests': return JustDigestsIterator(fp) raise ValueError, "unknown input style" class JustDigestsIterator(BasicIterator): __slots__ = ['fp'] def __init__(self, fp): self.fp = fp def next(self): l = fp.readline() if not l: raise StopIteration return l.rstrip() class MailboxDigester(BasicIterator): __slots__ = ['mbox', 'digest_spec', 'seekable'] def __init__(self, fp, digest_spec, seekable=False): import mailbox self.mbox = mailbox.PortableUnixMailbox(fp, rfc822BodyCleaner) self.digest_spec = digest_spec self.seekable = seekable def next(self): try: next_msg = self.mbox.next() except IOError: print "Error: Please feed mailbox files in on stdin, i.e." print " pyzor digest --mbox < my_mbox_file" next_msg = None if next_msg is None: raise StopIteration return DataDigester(next_msg, self.digest_spec, seekable=self.seekable).get_digest() class rfc822BodyCleaner(BasicIterator): __slots__ = ['fp', 'multifile', 'curfile', 'type'] def __init__(self, fp): msg = mimetools.Message(fp, seekable=0) # Default type is text. See #1529694. self.type = msg.getmaintype() or "text" self.multifile = None self.curfile = None # Check if we got a mail or not. Set type to binary if there is no # 'From:' header and type is text/plain with encoding 7bit. # 7bit is passed through anyway so nobody cares. if (not msg.has_key("From") and self.type == 'text' and msg.subtype == 'plain' and msg.getencoding() == '7bit'): self.type = 'binary' if self.type is '': self.type = 'text' if self.type == 'text': encoding = msg.getencoding() self.curfile = msg.fp if encoding != '7bit': # fix bad encoding name if encoding == '8bits': encoding = '8bit' import binascii self.curfile = tempfile.TemporaryFile() try: mimetools.decode(msg.fp, self.curfile, encoding) except binascii.Error, e: sys.stderr.write("%s: %s\n" % (e.__class__, e)) self.curfile = cStringIO.StringIO() except ValueError, e: #sys.stderr.write("%s: %s\n" % (e.__class__, e)) self.curfile = msg.fp except multifile.Error, e: #sys.stderr.write("%s: %s\n" % (e.__class__, e)) self.curfile = msg.fp try: self.curfile.seek(0) except: # If we get an error we'll pass the message anyway. pass elif self.type == 'multipart': try: self.multifile = multifile.MultiFile(msg.fp, seekable=False) self.multifile.push(msg.getparam('boundary')) self.multifile.next() self.curfile = self.__class__(self.multifile) except (TypeError, AttributeError, multifile.Error): # ignore errors, pass msg as is self.curfile = msg.fp self.type = 'binary' if self.type == 'text' or self.type == 'multipart': assert self.curfile is not None elif self.type == 'binary': try: fp.seek(0) except: pass self.curfile = fp else: assert self.curfile is None def readline(self): l = '' try: if self.type in ('text', 'multipart', 'binary'): l = self.curfile.readline() if self.type == 'multipart' and not l and self.multifile.next(): self.curfile = self.__class__(self.multifile) # recursion. Could get messy if # we get a bunch of empty multifile parts l = self.readline() except (TypeError, AttributeError, multifile.Error): pass return l def next(self): try: l = self.readline() except multifile.Error, e: sys.stderr.write("%s: %s\n" % (e.__class__, e)) raise StopIteration if not l: raise StopIteration return l class ClientRunner(object): __slots__ = ['routine', 'all_ok'] def __init__(self, routine): self.routine = routine self.setup() def setup(self): self.all_ok = True def run(self, server, varargs, kwargs=None): if kwargs is None: kwargs = {} message = "%s\t" % str(server) response = None try: response = apply(self.routine, varargs, kwargs) self.handle_response(response, message) except (CommError, KeyError, ValueError), e: sys.stderr.write(message + ("%s: %s\n" % (e.__class__.__name__, e))) self.all_ok = False def handle_response(self, response, message): """mesaage is a string we've built up so far""" if not response.is_ok(): self.all_ok = False sys.stdout.write(message + str(response.head_tuple()) + '\n') class CheckClientRunner(ClientRunner): __slots__ = ['found_hit', 'whitelisted'] # the number of wl-count it takes for the normal # count to be overriden wl_count_clears = 1 def setup(self): self.found_hit = False self.whitelisted = False super(CheckClientRunner, self).setup() def handle_response(self, response, message): message += "%s\t" % str(response.head_tuple()) if response.is_ok(): wl_count = int(response['WL-Count']) if wl_count > 0: count = 0 self.whitelisted = True else: count = int(response['Count']) if count > 0: self.found_hit = True message += "%d\t%d" % (count, wl_count) sys.stdout.write(message + '\n') else: sys.stderr.write(message) class InfoClientRunner(ClientRunner): def handle_response(self, response, message): message += "%s\n" % str(response.head_tuple()) if response.is_ok(): count = int(response['Count']) message += "\tCount: %d\n" % count if count > 0: for f in ('Entered', 'Updated', 'WL-Entered', 'WL-Updated'): if response.has_key(f): val = int(response[f]) if val == -1: stringed = 'Never' else: stringed = time.ctime(val) # we want to insert the wl-count before # our wl printouts if f is 'WL-Entered': message += ("\tWhiteList Count: %d\n" % int(response['WL-Count'])) message += ("\t%s: %s\n" % (f, stringed)) sys.stdout.write(message) else: sys.stderr.write(message) class Account(tuple): def __init__(self, v): self.validate() def validate(self): typecheck(self.username, pyzor.Username) typecheck(self.keystuff, Keystuff) def username(self): return self[0] username = property(username) def keystuff(self): return self[1] keystuff = property(keystuff) class Keystuff(tuple): """tuple of (salt, key). Each is a long. One or the other may be None, but not both.""" def __init__(self, v): self.validate() def validate(self): # When we support just leaving the salt in, this should # be removed if self[1] is None: raise ValueError, "no key information" for x in self: if not (isinstance(x, long) or x is None): raise ValueError, "Keystuff must be long's or None's" # make sure we didn't get all None's if not filter(lambda x: x is not None, self): raise ValueError, "keystuff can't be all None's" def from_hexstr(self, s): parts = s.split(',') if len(parts) != 2: raise ValueError, "invalid number of parts for keystuff; perhaps you forgot comma at beginning for salt divider?" return self(map(self.hex_to_long, parts)) from_hexstr = classmethod(from_hexstr) def hex_to_long(h): """Allows the argument to be an empty string""" if h is '': return None return long(h, 16) hex_to_long = staticmethod(hex_to_long) def salt(self): return self[0] salt = property(salt) def key(self): return self[1] key = property(key) class AccountsDict(dict): """Key is pyzor.Address, value is Account When getting, defaults to anonymous_account""" anonymous_account = Account((pyzor.anonymous_user, Keystuff((None, 0L)))) def __setitem__(self, k, v): typecheck(k, pyzor.Address) typecheck(v, Account) super(AccountsDict, self).__setitem__(k, v) def __getitem__(self, k): try: return super(AccountsDict, self).__getitem__(k) except KeyError: return self.anonymous_account class AccountsFile(object): """Iteration gives a tuple of (Address, Account) Layout of file is: host : port ; username : keystuff """ __slots__ = ['fp', 'lineno', 'output'] def __init__(self, fp): self.fp = fp self.lineno = 0 self.output = pyzor.Output() def __iter__(self): return self def next(self): while 1: orig_line = self.fp.readline() self.lineno += 1 if not orig_line: raise StopIteration line = orig_line.strip() if not line or line.startswith('#'): continue fields = line.split(':') fields = map(lambda x: x.strip(), fields) if len(fields) != 4: self.output.warn("account file: invalid line %d: wrong number of parts" % self.lineno) continue try: return (pyzor.Address((fields[0], int(fields[1]))), Account((Username(fields[2]), Keystuff.from_hexstr(fields[3])))) except ValueError, e: self.output.warn("account file: invalid line %d: %s" % (self.lineno, e)) def run(): ExecCall().run() def handle_timeout(signum, frame): raise TimeoutError def download(url, outfile): import urllib2 open(outfile, "wb").write(urllib2.urlopen(url).read()) pyzor-0.5.0/lib/pyzor/server.py0000644000076500000240000004424411176137165016061 0ustar tameyerstaff"""networked spam-signature detection server""" from __future__ import division import os import sys import SocketServer import time import gdbm import cStringIO import traceback import threading import pyzor from pyzor import * __author__ = pyzor.__author__ __version__ = pyzor.__version__ __revision__ = "$Id: server.py,v 1.29 2002-10-09 00:45:45 ftobin Exp $" class AuthorizationError(pyzor.CommError): """signature was valid, but not permitted to do the requested action""" pass class ACL(object): __slots__ = ['entries'] default_allow = False def __init__(self): self.entries = [] def add_entry(self, entry): typecheck(entry, ACLEntry) self.entries.append(entry) def allows(self, user, op): typecheck(user, Username) typecheck(op, Opname) for entry in self.entries: if entry.allows(user, op): return True if entry.denies(user, op): return False return self.default_allow class ACLEntry(tuple): all_keyword = 'all'.lower() def __init__(self, v): (user, op, allow) = v typecheck(user, Username) typecheck(op, Opname) assert bool(allow) == allow def user(self): return self[0] user = property(user) def op(self): return self[1] op = property(op) def allow(self): return self[2] allow = property(allow) def allows(self, user, op): return self._says(user, op, True) def denies(self, user, op): return self._says(user, op, False) def _says(self, user, op, allow): """If allow is True, we return true if and only if we allow user to do op. If allow is False, we return true if and only if we deny user to do op """ typecheck(user, Username) typecheck(op, Opname) assert bool(allow) == allow return (self.allow == allow and (self.user == user or self.user.lower() == self.all_keyword) and (self.op == op or self.op.lower() == self.all_keyword)) class AccessFile(object): # I started doing an iterator protocol for this, but it just # got too complicated keeping track of everything on the line __slots__ = ['file', 'output', 'lineno'] allow_keyword = 'allow' deny_keyword = 'deny' def __init__(self, f): self.output = Output() self.file = f self.lineno = 0 def feed_into(self, acl): typecheck(acl, ACL) for orig_line in self.file: self.lineno += 1 line = orig_line.strip() if not line or line.startswith('#'): continue parts = line.split(':') if len(parts) != 3: self.output.warn("access file: invalid number of parts in line %d" % self.lineno) continue (ops_str, users_str, allow_str) = parts ops = [] for op_str in ops_str.split(): try: op = Opname(op_str) except ValueError, e: self.output.warn("access file: invalid opname %s line %d: %s" % (repr(op_str), self.lineno, e)) else: ops.append(op) users = [] for u in users_str.split(): try: user = Username(u) except ValueError, e: self.output.warn("access file: invalid username %s line %d: %s" % (repr(u), self.lineno, e)) else: users.append(user) allow_str = allow_str.strip() if allow_str.lower() == self.allow_keyword: allow = True elif allow_str.lower() == self.deny_keyword: allow = False else: self.output.warn("access file: invalid allow/deny keyword %s line %d" % (repr(allow_str), self.lineno)) continue for op in ops: for user in users: acl.add_entry(ACLEntry((user, op, allow))) class Passwd(dict): def __setitem__(self, k, v): typecheck(k, pyzor.Username) typecheck(v, long) super(Passwd, self).__setitem__(k, v) class PasswdFile(BasicIterator): """Iteration gives (Username, long) objects Format of file is: user : key """ __slots__ = ['file', 'output', 'lineno'] def __init__(self, f): self.file = f self.output = Output() self.lineno = 0 def next(self): while True: orig_line = self.file.readline() self.lineno += 1 if not orig_line: raise StopIteration line = orig_line.strip() if not line or line.startswith('#'): continue fields = line.split(':') fields = map(lambda x: x.strip(), fields) if len(fields) != 2: self.output.warn("passwd line %d is invalid (wrong number of parts)" % self.lineno) continue try: return (Username(fields[0]), long(fields[1], 16)) except ValueError, e: self.output.warn("invalid passwd entry line %d: %s" % (self.lineno, e)) class Log(object): __slots__ = ['fp'] def __init__(self, fp=None): self.fp = fp def log(self, address, user=None, command=None, arg=None, code=None): # we don't use defaults because we want to be able # to pass in None if user is None: user = '' if command is None: command = '' if arg is None: arg = '' if code is None: code = -1 # We duplicate the time field merely so that # humans can peruse through the entries without processing ts = int(time.time()) if self.fp is not None: self.fp.write("%s\n" % ','.join((("%d" % ts), time.ctime(ts), user, address[0], command, repr(arg), ("%d" % code) ))) self.fp.flush() class Record(object): """Prefix conventions used in this class: r = report (spam) wl = whitelist """ __slots__ = ['r_count', 'r_entered', 'r_updated', 'wl_count', 'wl_entered', 'wl_updated', ] fields = ('r_count', 'r_entered', 'r_updated', 'wl_count', 'wl_entered', 'wl_updated', ) this_version = '1' # epoch seconds never = -1 def __init__(self, r_count=0, wl_count=0): self.r_count = r_count self.wl_count = wl_count self.r_entered = self.never self.r_updated = self.never self.wl_entered = self.never self.wl_updated = self.never def wl_increment(self): # overflow prevention if self.wl_count < sys.maxint: self.wl_count += 1 if self.wl_entered == self.never: self.wl_entered = int(time.time()) self.wl_update() def r_increment(self): # overflow prevention if self.r_count < sys.maxint: self.r_count += 1 if self.r_entered == self.never: self.r_entered = int(time.time()) self.r_update() def r_update(self): self.r_updated = int(time.time()) def wl_update(self): self.wl_updated = int(time.time()) def __str__(self): return "%s,%d,%d,%d,%d,%d,%d" \ % ((self.this_version,) + tuple(map(lambda x: getattr(self, x), self.fields))) def from_str(self, s): parts = s.split(',') dispatch = None version = parts[0] if len(parts) == 3: dispatch = self.from_str_0 elif version == '1': dispatch = self.from_str_1 else: raise StandardError, ("don't know how to handle db value %s" % repr(s)) return apply(dispatch, (s,)) from_str = classmethod(from_str) def from_str_0(self, s): r = Record() parts = s.split(',') fields = ('r_count', 'r_entered', 'r_updated') assert len(parts) == len(fields) for i in range(len(parts)): setattr(r, fields[i], int(parts[i])) return r from_str_0 = classmethod(from_str_0) def from_str_1(self, s): r = Record() parts = s.split(',')[1:] assert len(parts) == len(self.fields) for i in range(len(parts)): setattr(r, self.fields[i], int(parts[i])) return r from_str_1 = classmethod(from_str_1) class DBHandle(Singleton): __slots__ = ['output', 'initialized'] db_lock = threading.Lock() max_age = 3600*24*30*4 # 3 months db = None sync_period = 60 reorganize_period = 3600*24 # 1 day def __init__(self): assert self.db is not None, "database was not initialized" def initialize(self, fn, mode): self.output = Output() self.db = gdbm.open(fn, mode) self.start_reorganizing() self.start_syncing() initialize = classmethod(initialize) def apply_locking_method(self, method, varargs=(), kwargs={}): # just so we don't carry around a mutable kwargs if kwargs == {}: kwargs = {} self.output.debug("acquiring lock") self.db_lock.acquire() self.output.debug("acquired lock") try: result = apply(method, varargs, kwargs) finally: self.output.debug("releasing lock") self.db_lock.release() self.output.debug("released lock") return result apply_locking_method = classmethod(apply_locking_method) def __getitem__(self, key): return self.apply_locking_method(self._really_getitem, (key,)) def _really_getitem(self, key): return self.db[key] def __setitem__(self, key, value): self.apply_locking_method(self._really_setitem, (key, value)) def _really_setitem(self, key, value): self.db[key] = value def start_syncing(self): self.apply_locking_method(self._really_sync) self.sync_timer = threading.Timer(self.sync_period, self.start_syncing) self.sync_timer.start() start_syncing = classmethod(start_syncing) def _really_sync(self): self.db.sync() _really_sync = classmethod(_really_sync) def start_reorganizing(self): self.apply_locking_method(self._really_reorganize) self.reorganize_timer = threading.Timer(self.reorganize_period, self.start_reorganizing) self.reorganize_timer.start() start_reorganizing = classmethod(start_reorganizing) def _really_reorganize(self): self.output.debug("reorganizing the database") key = self.db.firstkey() breakpoint = time.time() - self.max_age while key is not None: rec = Record.from_str(self.db[key]) delkey = None if rec.r_updated < breakpoint: self.output.debug("deleting key %s" % key) delkey = key key = self.db.nextkey(key) if delkey: del self.db[delkey] self.db.reorganize() _really_reorganize = classmethod(_really_reorganize) class Server(SocketServer.ThreadingUDPServer, object): max_packet_size = 8192 time_diff_allowance = 180 def __init__(self, address, log): typecheck(log, Log) self.output = Output() RequestHandler.output = self.output RequestHandler.log = log self.output.debug('listening on %s' % str(address)) super(Server, self).__init__(address, RequestHandler) def serve_forever(self): self.pid = os.getpid() super(Server, self).serve_forever() def replace_log(self, newlog): typecheck(newlog, Log) RequestHandler.log = newlog self.output.debug("changing logfile") class RequestHandler(SocketServer.DatagramRequestHandler, object): def setup(self): super(RequestHandler, self).setup() # This is to work around a bug in current versions # of Python. The bug has been reported, and fixed # in Python's CVS. self.wfile = cStringIO.StringIO() self.client_address = Address(self.client_address) self.out_msg = Response() self.user = None self.op = None self.op_arg = None self.out_code = None self.msg_thread = None def handle(self): try: self._really_handle() except UnsupportedVersionError, e: self.handle_error(505, "Version Not Supported: %s" % e) except NotImplementedError, e: self.handle_error(501, "Not implemented: %s" % e) except (ProtocolError, KeyError), e: # We assume that KeyErrors are due to not # finding a key in the RFC822 message self.handle_error(400, "Bad request: %s" % e) except AuthorizationError, e: self.handle_error(401, "Unauthorized: %s" % e) except SignatureError, e: self.handle_error(401, "Unauthorized, Signature Error: %s" % e) except Exception, e: self.handle_error(500, "Internal Server Error: %s" % e) traceback.print_exc() self.out_msg.setdefault('Code', str(self.out_msg.ok_code)) self.out_msg.setdefault('Diag', 'OK') self.out_msg.init_for_sending() self.log.log(self.client_address, self.user, self.op, self.op_arg, int(self.out_msg['Code'])) msg_str = str(self.out_msg) self.output.debug("sending: %s" % repr(msg_str)) self.wfile.write(msg_str) def _really_handle(self): """handle() without the exception handling""" self.output.debug("received: %s" % repr(self.packet)) signed_msg = MacEnvelope(self.rfile) self.user = Username(signed_msg['User']) if self.user != pyzor.anonymous_user: if self.server.passwd.has_key(self.user): signed_msg.verify_sig(self.server.passwd[self.user]) else: raise SignatureError, "unknown user" self.in_msg = signed_msg.get_submsg(pyzor.Request) self.msg_thread = self.in_msg.get_thread() # We take the int() of the proto versions because # if the int()'s are the same, then they should be compatible if int(self.in_msg.get_protocol_version()) != int(proto_version): raise UnsupportedVersionError self.out_msg.set_thread(self.msg_thread) self.op = Opname(self.in_msg.get_op()) if not self.server.acl.allows(self.user, self.op): raise AuthorizationError, "user is unauthorized to request the operation" self.output.debug("got a %s command from %s" % (self.op, self.client_address)) if not self.dispatches.has_key(self.op): raise NotImplementedError, "requested operation is not implemented" dispatch = self.dispatches[self.op] if dispatch is not None: apply(dispatch, (self,)) def handle_error(self, code, s): self.out_msg = ErrorResponse(code, s) if self.msg_thread is None: self.out_msg.set_thread(ThreadId(0)) else: self.out_msg.set_thread(self.msg_thread) def handle_check(self): digest = self.in_msg['Op-Digest'] self.op_arg = digest self.output.debug("request to check digest %s" % digest) db = DBHandle() try: rec = Record.from_str(db[digest]) r_count = rec.r_count wl_count = rec.wl_count except KeyError: r_count = 0 wl_count = 0 self.out_msg['Count'] = "%d" % r_count self.out_msg['WL-Count'] = "%d" % wl_count def handle_report(self): digest = self.in_msg['Op-Digest'] self.op_arg = digest self.output.debug("request to report digest %s" % digest) db = DBHandle() try: rec = Record.from_str(db[digest]) except KeyError: rec = Record() rec.r_increment() db[digest] = str(rec) def handle_whitelist(self): digest = self.in_msg['Op-Digest'] self.op_arg = digest self.output.debug("request to whitelist digest %s" % digest) db = DBHandle() try: rec = Record.from_str(db[digest]) except KeyError: rec = Record() rec.wl_increment() db[digest] = str(rec) def handle_info(self): digest = self.in_msg['Op-Digest'] self.op_arg = digest self.output.debug("request to check digest %s" % digest) db = DBHandle() try: record = Record.from_str(db[digest]) except KeyError: record = Record() r_count = record.r_count wl_count = record.wl_count self.out_msg['Entered'] = "%d" % record.r_entered self.out_msg['Updated'] = "%d" % record.r_updated self.out_msg['WL-Entered'] = "%d" % record.wl_entered self.out_msg['WL-Updated'] = "%d" % record.wl_updated self.out_msg['Count'] = "%d" % r_count self.out_msg['WL-Count'] = "%d" % wl_count dispatches = { 'check': handle_check, 'report': handle_report, 'ping': None, 'info': handle_info, 'whitelist': handle_whitelist, } pyzor-0.5.0/MANIFEST0000644000076500000240000000066011176137136013371 0ustar tameyerstaffCOPYING ChangeLog INSTALL MANIFEST NEWS README THANKS UPGRADING docs/pyzor.1 docs/pyzord.1 docs/usage.html lib/pyzor/__init__.py lib/pyzor/client.py lib/pyzor/server.py scripts/access scripts/bob/accounts scripts/bob/servers scripts/config scripts/passwd scripts/pyzor scripts/pyzord scripts/servers scripts/test.in.0 scripts/test.in.mbox scripts/test.sh t/atomic t/atomic.not t/multipart t/multipart.expected setup.py unittests.py pyzor-0.5.0/NEWS0000644000076500000240000001351211176137136012737 0ustar tameyerstaffNoteworthy changes in 0.5.0 ----------------------------------------------------------------- Note that the majority of changes in this release were contributed back from the Debian pyzor package. * Man pages for pyzor and pyzord. * Changing back to signals for database locking, rather than threads. It is likely that signals will be removed again in the future, but the existing threading changes caused problems. * Basic checks on the results of "discover". * Extended mbox support throughout the library. * Better handling on unknown encodings. * Added a --log option to log to a file. * Better handling of command-line options. * Improved error handling. Noteworthy changes in 0.4.x ----------------------------------------------------------------- * pyzor client now more gracefully handles base64 and multipart decoding errors, so that it can be used over an mbox. * pyzor client has new config file option in the [client] section, Timeout, which specifies a timeout in seconds for queries to come back to the client. * pyzord no longer daemonizes itself, and now writes it logging to standard output. * The following server config options no longer have effect: PidFile, LogFile. * Upped the allowed signed timestamp difference to be up to 5 minutes (up from 3 minutes). * Removed the 'shutdown' command; implementation of 'meta' commands need to be re-thought. * Rewrite of threads locking to access the database. * pyzord no longer handles USR1 signals; instead, it now automatically reorganizes and cleans-up the database daily. * Client code now uses threading to catch timeouts, rather than an alarm signal. Noteworthy changes in 0.4.0 ----------------------------------------------------------------- * Messages are now decoded if they are encoded, and subparts that are not encoded text/* are ignored. Currently base64, quoted-printable, and uuencode is supported. * Message normalization now removes HTML tags (irregardless of Content-Type). * Message lines with less than 8 chars after normalization are now not included in digests. * Messages having less than or equal to 4 lines are entirely digested. * Implemented 'digest' command, which simply prints out the digest(s) of the messages encountered. * Implemented 'predigest' command which prints out the data that is actually digested in a message. * If HOME is unspecified and no --homedir is given, The the config directory is /etc/pyzor * If the pyzord process receives a HUP signal, it re-opens the logfile. Noteworthy changes in 0.3.1 ----------------------------------------------------------------- * Fixed bug where if pyzor would send reports or whitelists to each server N times, where N is the number of servers. * Server now keeps database file open, instead of re-opening it on each request. * pyzord.log now includes response code. Noteworthy changes in 0.3.0 ----------------------------------------------------------------- * Pyzor now requires Python 2.2.1. * The protocol is not backwards compatible, so please remove old ~/.pyzor/servers files, and they will be refreshened to point to new servers. * The pyzor system now has accounts, access controls on users. anonymous users by default can do ['check', 'report', 'ping', 'info']. For more information on this, please refer to the documentation. * Documentation has moved from the source files (e.g., 'pydoc pyzor') into a separate XHTML document, located in docs/usage.html, and normally installed into a location such as /usr/share/doc/pyzor * Messages are authenticated using digest-signing, similar to HTTP-digest authentication. This is a is a shared-secret scheme, but the secret is very hard to recover from what is passed in the signature. * Whitelisting messages is now possible. * An 'info' command is no implemented This returns extra info about any digest, such as when it was first entered and last updated. * a 'genkey' command has been implemented for the client; this is used to create a (salt, key) string used for authentication. * a 'shutdown' commmand has been implemented, which can be used to shutdown a server. * Expiring of digests using a USR1 signal has been removed for now. In the future a client/server message will be likely be implemented for this functionality. * pyzrod logfile now contains a human-readable timestamp field in addition to the epoch-seconds field. Noteworthy changes in 0.2.1 ----------------------------------------------------------------- * Fixed major bug where the incorrect exit code is given. Noteworthy changes in 0.2.0 ----------------------------------------------------------------- * Protocol break. Old clients will not work with new servers, and vice versa. * ~/.pyzor is now a directory, with ~/.pyzor/config containing configuration directives. ~/.pyzor/servers contains a list of servers. * pyzord's command-line interface has changed, now being primarily configured in ~/.pyzor/config. * pyzord now does logging (~/.pyzor/pyzord.log) and has a pidfile (~/.pyzor/pyzord.pid). * Debugging for client and server improved. * Client now contacts each server listed when doing a check/report/ping. * Can now be used with ReadyExec, http://readyexec.sourceforge.net/ Documentation on how to use ReadyExec is in the pyzor documentation. Noteworthy changes in 0.1.1 ----------------------------------------------------------------- * Fixed problem when trying to report messages in non-unix mailbox format. * Added --mbox option for 'pyzor report' for when reporting entire mailboxes. * No changes were made in the server portion. Noteworthy changes in 0.1.0 ----------------------------------------------------------------- * Initial release. pyzor-0.5.0/PKG-INFO0000644000076500000240000000055111176137366013341 0ustar tameyerstaffMetadata-Version: 1.0 Name: pyzor Version: 0.5.0 Summary: networked spam-signature detection Home-page: http://pyzor.sourceforge.net/ Author: Frank J. Tobin Author-email: ftobin@neverending.org License: GPL Description: Pyzor is spam-blocking networked system that uses spam signatures to identify them. Keywords: spam Platform: POSIX pyzor-0.5.0/README0000644000076500000240000000034411176137136013117 0ustar tameyerstaffPyzor is a Python implementation of a spam-blocking networked system that use spam signatures to identify them. http://pyzor.sourceforge.net/ See INSTALL for install documentation. See docs/usage.html for usage documentation. pyzor-0.5.0/scripts/0000755000076500000240000000000011176137366013732 5ustar tameyerstaffpyzor-0.5.0/scripts/access0000644000076500000240000000006111176137136015106 0ustar tameyerstaffping check : anonymous : allow ALL : bob : allow pyzor-0.5.0/scripts/bob/0000755000076500000240000000000011176137366014474 5ustar tameyerstaffpyzor-0.5.0/scripts/bob/accounts0000644000076500000240000000015311176137136016230 0ustar tameyerstaff127.0.0.1 : 9999 : bob : 227bfb58efaba7c582d9dcb66ab2063d38df2923,8da9f54058c34e383e997f45d6eb74837139f83b pyzor-0.5.0/scripts/bob/servers0000644000076500000240000000001711176137136016101 0ustar tameyerstaff127.0.0.1:9999 pyzor-0.5.0/scripts/config0000644000076500000240000000016711176137136015121 0ustar tameyerstaff[server] Port = 9999 ListenAddress = 127.0.0.1 AccessFile = access PasswdFile = passwd [client] ServersFile = servers pyzor-0.5.0/scripts/passwd0000644000076500000240000000005711176137136015153 0ustar tameyerstaffbob : 8da9f54058c34e383e997f45d6eb74837139f83b pyzor-0.5.0/scripts/pyzor0000755000076500000240000000014011176137136015031 0ustar tameyerstaff#!/usr/bin/python import os # set umask os.umask(0077) import pyzor.client pyzor.client.run() pyzor-0.5.0/scripts/pyzord0000755000076500000240000000555611176137176015221 0ustar tameyerstaff#!/usr/bin/python import os import os.path import sys import getopt import pyzor import pyzor.server import ConfigParser _author__ = pyzor.__author__ __version__ = pyzor.__version__ __revision__ = "$Id: pyzord,v 1.23 2002-10-09 00:33:44 ftobin Exp $" progname = 'pyzord' default_anonymous_allows = map(pyzor.Opname, ['check', 'report', 'ping', 'info']) def usage(): sys.stderr.write("usage: %s [-d] [--homedir dir]\n" % progname) sys.exit(1) def load_access_file(access_fn, server): server.acl = pyzor.server.ACL() if os.path.exists(access_fn): pyzor.server.AccessFile(open(access_fn)).feed_into(server.acl) else: output.warn("%s does not exist; using default ACL: allowing anonymous to do %s" % (access_fn, default_anonymous_allows)) for op in default_anonymous_allows: server.acl.add_entry(pyzor.server.ACLEntry((pyzor.anonymous_user, op, True))) def load_passwd_file(access_fn, server): server.passwd = pyzor.server.Passwd() if os.path.exists(passwd_fn): for user, key in pyzor.server.PasswdFile(open(passwd_fn)): server.passwd[user] = key ######################################################################## # functions above, run below # set umask os.umask(0077) debug = 0 (options, args) = getopt.getopt(sys.argv[1:], 'dh:', ['homedir=']) if len(args) != 0: usage() specified_homedir = None for (o, v) in options: if o == '-d': debug = 1 elif o == '-h': usage() elif o == '--homedir': specified_homedir = v homedir = pyzor.get_homedir(specified_homedir) if not os.path.exists(homedir): os.mkdir(homedir) defaults = {'port': '24441', 'listenaddress': '0.0.0.0', 'digestdb': 'pyzord.db', 'passwdfile': 'pyzord.passwd', 'accessfile': 'pyzord.access', 'CleanupAge': "%d" % pyzor.server.DBHandle.max_age, } config = pyzor.Config(homedir) config.add_section('server') for k, v in defaults.items(): config.set('server', k, v) config.read(os.path.join(homedir, 'config')) port = config.getint('server', 'port') listen_adr = config.get('server', 'ListenAddress') dbfile = config.get_filename('server', 'DigestDB') passwd_fn = config.get_filename('server', 'passwdfile') access_fn = config.get_filename('server', 'accessfile') pyzor.server.DBHandle.max_age = config.getint('server', 'CleanupAge') output = pyzor.Output(debug=debug) pyzor.server.DBHandle.initialize(dbfile, 'c') server = pyzor.server.Server((listen_adr, port), pyzor.server.Log(sys.stdout)) load_passwd_file(passwd_fn, server) load_access_file(access_fn, server) server.serve_forever() pyzor-0.5.0/scripts/servers0000644000076500000240000000001711176137136015337 0ustar tameyerstaff127.0.0.1:9999 pyzor-0.5.0/scripts/test.in.00000644000076500000240000000230311176137136015370 0ustar tameyerstaffNewsgroups: 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 ffsome stuff and blarg blarg@blddddda emad foobar http:/ barbar sdf sdf ahrm THE LESSER-KNOWN PROGRAMMING LANGUAGES #8: LAIDBACK This language was developed at the Marin County Center for T'ai Chi, Mellowness and Computer Programming (now defunct), as an alternative to the more intense atmosphere in nearby Silicon Valley. The center was ideal for programmers who liked to soak in hot tubs while they worked. Unfortunately few programmers could survive there because the center outlawed Pizza and Coca-Cola in favor of Tofu and Perrier. Many mourn the demise of LAIDBACK because of its reputation as a gentle and non-threatening language since all error messages are in lower case. For example, LAIDBACK responded to syntax errors with the message: "i hate to bother you, but i just can't relate to that. can you find the time to try it again?" -- Frank Tobin http://www.neverending.org/~ftobin/ pyzor-0.5.0/scripts/test.in.mbox0000644000076500000240000000340411176137136016201 0ustar tameyerstaffFrom nobody@mozilla.org Mon Apr 1 15:46:12 2002 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 "Nuclear war would mean abolition of most comforts, and disruption of normal routines, for children and adults alike." -- Willard F. Libby, "You *Can* Survive Atomic Attack" From nobody@mozilla.org Mon Apr 1 15:46:12 2002 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 ffsome stuff and blarg blarg@blddddda emad foobar http:/ barbar sdf sdf ahrm THE LESSER-KNOWN PROGRAMMING LANGUAGES #8: LAIDBACK This language was developed at the Marin County Center for T'ai Chi, Mellowness and Computer Programming (now defunct), as an alternative to the more intense atmosphere in nearby Silicon Valley. The center was ideal for programmers who liked to soak in hot tubs while they worked. Unfortunately few programmers could survive there because the center outlawed Pizza and Coca-Cola in favor of Tofu and Perrier. Many mourn the demise of LAIDBACK because of its reputation as a gentle and non-threatening language since all error messages are in lower case. For example, LAIDBACK responded to syntax errors with the message: "i hate to bother you, but i just can't relate to that. can you find the time to try it again?" -- Frank Tobin http://www.neverending.org/~ftobin/ pyzor-0.5.0/scripts/test.sh0000755000076500000240000000466111176137136015252 0ustar tameyerstaff#!/bin/sh # HOME so it finds the right .pyzor export HOME=. export PYTHONPATH=../lib start_server() { ./pyzord --homedir . > pyzord.log & PYZORD_PID=$! echo "started server with pid $PYZORD_PID" } pyzor() { ./pyzor --homedir . "$@" } pyzor_bob() { pyzor --homedir bob "$@" } check() { pyzor check < test.in.0 } check_bob() { pyzor_bob check < test.in.0 } kill_server() { echo "killing server (pid $PYZORD_PID)" kill $PYZORD_PID } fail() { echo "failed: $1" exit 1; } setcount() { count=`pyzor check < test.in.0 | cut -f 3` } setcount_bob() { count=`pyzor_bob check < test.in.0 | cut -f 3` } fail_cmp() { fail "got $1; expected $2" } rm -f pyzord.* echo "starting server" start_server # sleep to give it time to start listening sleep 2 [ ! -z $PYZORD_PID ] || fail "we didn't get a process id for the server!" kill -0 $PYZORD_PID || fail "process is dead" trap kill_server 0 echo "anonymous: pinging" pyzor ping || fail echo "anonymous: ensuring a count of 0 at start" setcount [ ${count:--1} = 0 ] || fail check && fail echo "bob: ensuring a count of 0 at start" setcount_bob [ ${count:--1} = 0 ] || fail check_bob && fail echo "anonymous: reporting" pyzor report < test.in.0 && fail echo "anonymous: reporting" pyzor report < test.in.0 && fail echo "bob: reporting" pyzor_bob report < test.in.0 || fail echo "bob: reporting" pyzor_bob report < test.in.0 || fail echo "anonymous: counting reports" setcount [ ${count:--1} = 2 ] || fail check || fail echo "bob: counting reports" setcount_bob [ ${count:--1} = 2 ] || fail_cmp ${count:--1} 2 check_bob || fail "checking failed" echo "bob: reporting a mailbox" pyzor_bob report --mbox < test.in.mbox || fail echo "bob: counting reports" setcount_bob [ ${count:--1} = 3 ] || fail_cmp ${count:--1} 3 check_bob || fail "checking exit code failed" echo "bob: getting info" # check exit pyzor_bob info < test.in.0 || fail # check lines [ `pyzor_bob info < test.in.0 | wc -l` = 7 ] || fail echo "anonymous: whitelisting" pyzor whitelist < test.in.0 && fail echo "bob: whitelisting" pyzor_bob whitelist < test.in.0 || fail echo "bob: getting info" # check exit pyzor_bob info < test.in.0 || fail # check lines [ `pyzor_bob info < test.in.0 | wc -l` = 7 ] || fail echo "bob: counting reports" setcount_bob [ ${count:--1} = 0 ] || fail_cmp ${count:--1} 0 check && fail echo "checking for logfile" [ -s pyzord.log ] || fail echo "passed" pyzor-0.5.0/setup.py0000644000076500000240000000170511176137136013753 0ustar tameyerstaffimport sys import distutils.core sys.path.insert(0, 'lib') import pyzor long_description = """ Pyzor is spam-blocking networked system that uses spam signatures to identify them. """ distutils.core.setup( name = 'pyzor', version = pyzor.__version__, description = 'networked spam-signature detection', long_description = long_description, author = 'Frank J. Tobin', author_email = 'ftobin@neverending.org', license = 'GPL', platforms = 'POSIX', keywords = 'spam', url = 'http://pyzor.sourceforge.net/', scripts=['scripts/pyzor', 'scripts/pyzord'], package_dir = {'': 'lib'}, packages = ['pyzor'], data_files=[('share/doc/pyzor', ['docs/usage.html'])], ) pyzor-0.5.0/t/0000755000076500000240000000000011176137366012506 5ustar tameyerstaffpyzor-0.5.0/t/atomic0000644000076500000240000000021311176137136013674 0ustar tameyerstaffFoo: bar Baz: bam It is contrary to reasoning to say that there is a vacuum or space in which there is absolutely nothing. -- Descartes pyzor-0.5.0/t/atomic.not0000644000076500000240000000112711176137136014500 0ustar tameyerstaffFoo: bar Baz: bam If builders built buildings the way programmers write programs, Jolt Cola would be a Fortune-500 company. If builders built buildings the way programmers write programs, you'd be able to buy a nice little colonial split-level at Babbages for $34.95. If programmers wrote programs the way builders build buildings, we'd still be using autocoder and running compile decks. -- Love is always open arms. With arms open you allow love to come and go as it wills, freely, for it will do so anyway. If you close your arms about love you'll find you are left only holding yourself. pyzor-0.5.0/t/multipart0000644000076500000240000003050111176137136014444 0ustar tameyerstaffFrom ftobin@localhost Thu Aug 22 17:04:49 2002 -0400 Status: X-Status: X-Keywords: Newsgroups: Date: Thu, 22 Aug 2002 17:04:49 -0400 (EDT) From: Frank Tobin To: Frank Tobin Subject: test of MIME and multipart for Pyzor Fcc: sent-mail Message-ID: X-Cursor-Pos: : 244 X-Our-Headers: X-Bogus X-Bogus: aaron7@neverending.org MIME-Version: 1.0 Content-Type: MULTIPART/MIXED; BOUNDARY="-827856688-1414219155-1030049918=:7686" Content-ID: ---827856688-1414219155-1030049918=:7686 Content-Type: TEXT/PLAIN; CHARSET=US-ASCII Content-ID: Three rules for sounding like an expert: (1) Oversimplify your explanations to the point of uselessness. (2) Always point out second-order effects, but never point out when they can be ignored. (3) Come up with three rules of your own. ---827856688-1414219155-1030049918=:7686 Content-Type: TEXT/HTML; NAME="index.html" Content-Transfer-Encoding: BASE64 Content-ID: Content-Description: Content-Disposition: ATTACHMENT; FILENAME="index.html" PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9D VFlQRSBodG1sIFBVQkxJQyAiLS8vVzNDLy9EVEQgWEhUTUwgMS4xLy9FTiIg Imh0dHA6Ly93d3cudzMub3JnL1RSL3hodG1sMTEvRFREL3hodG1sMTEuZHRk Ij4NCjxodG1sIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1s IiB4bWw6bGFuZz0iZW4tdXMiPg0KDQo8aGVhZD4NCiAgPHRpdGxlPkRla2Fu J3MgU29sYWNlIC0gSW50cm9kdWN0aW9uPC90aXRsZT4NCg0KICA8bWV0YSBu YW1lPSJhdXRob3IiIGNvbnRlbnQ9IkZyYW5rIFRvYmluLCBEZWthbiIgLz4N CiAgPG1ldGEgbmFtZT0ia2V5d29yZHMiIGNvbnRlbnQ9IkZyYW5rIFRvYmlu LCBEZWthbiwgZnRvYmluIiAvPg0KDQogIDxsaW5rIHJldj0iTWFkZSIgaHJl Zj0ibWFpbHRvOmZ0b2JpbkBuZXZlcmVuZGluZy5vcmciIC8+DQogIDxsaW5r IHJlbD0iQ29weXJpZ2h0IiBocmVmPSIjY29weXJpZ2h0IiAvPg0KICA8bGlu ayByZWw9IlN0YXJ0IiBocmVmPSIvfmZ0b2Jpbi8iIC8+DQogIDxsaW5rIHJl bD0iU2VhcmNoIiBocmVmPSJodHRwOi8vd3d3Lmdvb2dsZS5jb20vYWR2YW5j ZWRfc2VhcmNoP3E9c2l0ZTpodHRwOi8vd3d3Lm5ldmVyZW5kaW5nLm9yZytm dG9iaW4iIC8+DQoNCiAgPGxpbmsgcmVsPSJTdHlsZVNoZWV0IiB0aXRsZT0i RGVmYXVsdCIgaHJlZj0iL35mdG9iaW4vc3R5bGUvZ3JheWlzaC5jc3MiIHR5 cGU9InRleHQvY3NzIiAvPg0KICA8bGluayByZWw9IkFsdGVybmF0ZSBTdHls ZXNoZWV0IiB0aXRsZT0iQnJpZ2h0IiBocmVmPSIvfmZ0b2Jpbi9zdHlsZS9i cmlnaHQuY3NzIiB0eXBlPSJ0ZXh0L2NzcyIgLz4NCiAgPGxpbmsgcmVsPSJB bHRlcm5hdGUgU3R5bGVTaGVldCIgdGl0bGU9Ik1pZG5pZ2h0IiBocmVmPSIv fmZ0b2Jpbi9zdHlsZS9taWRuaWdodC5jc3MiIHR5cGU9InRleHQvY3NzIiAv Pg0KICANCg0KICA8bGluayByZWw9IkJvb2ttYXJrIiB0aXRsZT0iSW50cm9k dWN0aW9uIiBocmVmPSIvfmZ0b2Jpbi8iIC8+DQogIDxsaW5rIHJlbD0iQm9v a21hcmsiIHRpdGxlPSJSZXNvdXJjZXMiIGhyZWY9Ii9+ZnRvYmluL3Jlc291 cmNlcy8iIC8+DQogIDxsaW5rIHJlbD0iQm9va21hcmsiIHRpdGxlPSJQcm9q ZWN0cyIgaHJlZj0iL35mdG9iaW4vcHJvamVjdHMvIiAvPg0KICA8bGluayBy ZWw9IkJvb2ttYXJrIiB0aXRsZT0iUGVybCBUaXBzIiBocmVmPSIvfmZ0b2Jp bi9wZXJsX3RpcHMvIiAvPg0KICA8bGluayByZWw9IkJvb2ttYXJrIiB0aXRs ZT0iT3BpbmlvbiIgaHJlZj0iL35mdG9iaW4vb3Bpbmlvbi8iIC8+DQogIDxs aW5rIHJlbD0iQm9va21hcmsiIHRpdGxlPSJSZXN1bWUiIGhyZWY9Ii9+ZnRv YmluL3Jlc3VtZS8iIC8+DQogIDxsaW5rIHJlbD0iQm9va21hcmsiIHRpdGxl PSJBYm91dCIgaHJlZj0iL35mdG9iaW4vYWJvdXQvIiAvPg0KDQogIDxsaW5r IHJlbD0ibWV0YSIgaHJlZj0iL35mdG9iaW4vbWV0YWRhdGEvc3RhbmRhcmQu cmRmIiAvPg0KICA8bGluayByZWw9Imljb24iIGhyZWY9Ii9+ZnRvYmluL2dy YXBoaWNzL2RjbXMtMTYucG5nIiB0eXBlPSJpbWFnZS9wbmciIC8+DQoNCjwv aGVhZD4NCg0KPGJvZHk+DQoNCjxoMSBjbGFzcz0ic2l0ZS1iYW5uZXIiPkRl a2FuJ3MgU29sYWNlPC9oMT4NCg0KDQoNCjx1bCBjbGFzcz0ibmF2YmFyIj4N Cg0KICA8bGk+PGEgaHJlZj0iL35mdG9iaW4vIj5JbnRyb2R1Y3Rpb248L2E+ PC9saT4NCiAgPGxpPjxhIGhyZWY9Ii9+ZnRvYmluL3Jlc291cmNlcy8iPlJl c291cmNlczwvYT48L2xpPg0KICA8bGk+PGEgaHJlZj0iL35mdG9iaW4vcHJv amVjdHMvIj5Qcm9qZWN0czwvYT48L2xpPg0KICA8bGk+PGEgaHJlZj0iL35m dG9iaW4vcGVybF90aXBzLyI+UGVybCBUaXBzPC9hPjwvbGk+DQogIDxsaT48 YSBocmVmPSIvfmZ0b2Jpbi9vcGluaW9uLyI+T3BpbmlvbjwvYT48L2xpPg0K ICA8bGk+PGEgaHJlZj0iL35mdG9iaW4vcmVzdW1lLyI+UmVzdW1lPC9hPjwv bGk+DQogIDxsaT48YSBocmVmPSIvfmZ0b2Jpbi9hYm91dC8iPkFib3V0PC9h PjwvbGk+DQoNCjwvdWw+DQoNCg0KDQogIDxoMT5JbnRyb2R1Y3Rpb248L2gx Pg0KDQogIDxkaXY+DQogICAgPGltZyBzcmM9ImdyYXBoaWNzL2RjbXMucG5n IiBhbHQ9IiIgdGl0bGU9IkRyYWNvbmlzIENvbWJpbmUgTXVzdGVyZWQgU29s ZGllcnkgbG9nbyIgbG9uZ2Rlc2M9Imh0dHA6Ly93d3cuYmF0dGxldGVjaGNl bnRyYWwubnUvSGlzdG9yeS9kY2hpc3RvcnkuaHRtIiBzdHlsZT0iZmxvYXQ6 IHJpZ2h0IiAvPg0KICA8L2Rpdj4NCg0KICA8cD4NCiAgICBJIGhvcGUgeW91 ciBzdGF5IGluIFNvbGFjZSBoZXJlIHdpbGwNCiAgICBsZXQgeW91IGxlYXJu IGEgYml0IG1vcmUgYWJvdXQgbWUsIGFuZCB0aGF0IA0KICAgIHlvdSBtYXkg ZmluZCBzb21ldGhpbmcgaW50ZXJlc3RpbmcgaGVyZS4NCiAgPC9wPg0KDQog IDxoMj5PdGhlciBSZWZlcmVuY2VzIHRvIE1lPC9oMj4NCg0KICA8cD4NCiAg ICBNeSA8YSBocmVmPSJodHRwOi8vc291cmNlZm9yZ2UubmV0L3VzZXJzL2Z0 b2Jpbi8iPmRldmVsb3BlciBhY2NvdW50PC9hPg0KICAgIGF0IDxhIGhyZWY9 Imh0dHA6Ly9zb3VyY2Vmb3JnZS5uZXQvIj5Tb3VyY2VGb3JnZTwvYT4gbWF5 IGJlDQogICAgb2YgaW50ZXJlc3Q7IGl0IGNvbnRhaW5zIG15DQogICAgPGEg aHJlZj0iaHR0cDovL3NvdXJjZWZvcmdlLm5ldC9kZXZlbG9wZXIvZGlhcnku cGhwP2RpYXJ5X3VzZXI9NzU3NyI+cHVibGljDQogICAgICBkaWFyeTwvYT4s IHdoaWNoIGlzIGdlbmVyYWxseSByZWxhdGVkIHRvDQogICAgZGV2ZWxvcG1l bnQgc3R1ZmYgYW5kIHByb2plY3RzIEkgYW0gd29ya2luZyBvbi4NCiAgPC9w Pg0KDQogIDxoMj5JbnRlcmVzdGluZyBTYXlpbmdzPC9oMj4NCg0KICA8cCBj bGFzcz0iZ29vZC1xdW90ZSI+DQogICAgRXN0IFN1bGFydXMgb3RoIE1pdGhh cy4NCiAgPC9wPg0KDQogIDxwPg0KICAgIDxxIGNsYXNzPSJnb29kLXF1b3Rl IiBzdHlsZT0iZGlzcGxheTogYmxvY2siPk9uZSdzIHJlYWwgbGlmZSBpcyBz byBvZnRlbiB0aGUgbGlmZSB0aGF0IG9uZSBkb2VzIG5vdCBsZWFkLjwvcT4N CiAgICAgIDxjaXRlIGNsYXNzPSJxdW90ZS1hdHRyaWJ1dGlvbiI+DQoJPGEg aHJlZj0iaHR0cDovL3d3dy5jcC10ZWwubmV0L21pbGxlci9CaWxMZWUvcXVv dGVzL1dpbGRlLmh0bWwiPk9zY2FyIFdpbGRlPC9hPiAoMTg1NC0xOTAwKSwg QW5nbG8tSXJpc2ggcGxheXdyaWdodCwgYXV0aG9yDQogICAgICA8L2NpdGU+ DQogIDwvcD4NCg0KICA8cD4NCiAgICA8cSBjbGFzcz0iZ29vZC1xdW90ZSIg c3R5bGU9ImRpc3BsYXk6IGJsb2NrIj5UbyBsZWFybiB3aGF0IGlzIGdvb2Qg YW5kIHdoYXQgaXMgdG8gYmUgdmFsdWVkLA0KICAgICAgdGhvc2UgdHJ1dGhz IHdoaWNoIGNhbm5vdCBiZSBzaGFrZW4gb3IgY2hhbmdlZC48L3E+DQogICAg ICA8Y2l0ZSBjbGFzcz0icXVvdGUtYXR0cmlidXRpb24iPjxhIGhyZWY9Imh0 dHA6Ly9zaXJydXMuY3lhbi5jb20vT25saW5lL0QlMjduaS9Cb29rcy9Cb29r QXRydXMiPk15c3Q6IFRoZSBCb29rIG9mIEF0cnVzPC9hPjwvY2l0ZT4NCiAg PC9wPg0KDQoNCjxkaXYgY2xhc3M9ImZvb3RlciI+DQogICAgDQo8aHIgLz4N Cg0KPGFkZHJlc3M+PGEgaHJlZj0ibWFpbHRvOmZ0b2JpbkBuZXZlcmVuZGlu Zy5vcmciPkZyYW5rIEouIFRvYmluPC9hPjwvYWRkcmVzcz4NCiAgICANCjxw IGlkPSJjb3B5cmlnaHQiPg0KQ29udGVudCBpcyByZWxlYXNlZCB1bmRlciB0 aGUNCjxhIGhyZWY9Imh0dHA6Ly93d3cuZ251Lm9yZy9jb3B5bGVmdC9mZGwu aHRtbCI+R05VIEZyZWUgRG9jdW1lbnRhdGlvbiBMaWNlbnNlPC9hPiwNCmNv cHlyaWdodCBoZWxkIGJ5IDxhIGhyZWY9Im1haWx0bzpmdG9iaW5AbmV2ZXJl bmRpbmcub3JnIj5GcmFuayBKLiBUb2JpbjwvYT4uDQo8L3A+DQoNCjxwPlRo aXMgcGFnZSBpcyB3cml0dGVuIGluDQogIDxhIGhyZWY9Imh0dHA6Ly92YWxp ZGF0b3IudzMub3JnL2NoZWNrL3JlZmVyZXIiPjxpbWcgc3JjPSJodHRwOi8v d3d3LnczLm9yZy9JY29ucy92YWxpZC14aHRtbDExIiBhbHQ9InZhbGlkIFhI VE1MIiBjbGFzcz0idGFnIiAvPjwvYT4NCiAgYW5kDQogIDxhIGhyZWY9Imh0 dHA6Ly9qaWdzYXcudzMub3JnL2Nzcy12YWxpZGF0b3IvY2hlY2svcmVmZXJl ciI+PGltZyBzcmM9Imh0dHA6Ly9qaWdzYXcudzMub3JnL2Nzcy12YWxpZGF0 b3IvaW1hZ2VzL3Zjc3MiIGFsdD0idmFsaWQgQ1NTIiBjbGFzcz0idGFnIiAv PjwvYT4uDQo8L3A+DQogIA0KPC9kaXY+DQoNCjwhLS0NCiAgRG9uJ3Qgc2Vu ZCBlbWFpbCB0byBhYXJvbjdAbmV2ZXJlbmRpbmcub3JnIG9yIG1haWx0bzph YXJvbjdAbmV2ZXJlbmRpbmcub3JnDQotLT4NCg0KPC9ib2R5Pg0KPC9odG1s Pg0KDQo= ---827856688-1414219155-1030049918=:7686 Content-Type: TEXT/PLAIN; CHARSET=US-ASCII; NAME=fortune Content-Transfer-Encoding: BASE64 Content-ID: Content-Description: Content-Disposition: ATTACHMENT; FILENAME=fortune SHVtb3IgaW4gdGhlIENvdXJ0Og0KUTogLi4uYW55IHN1Z2dlc3Rpb25zIGFz IHRvIHdoYXQgcHJldmVudGVkIHRoaXMgZnJvbSBiZWluZyBhIG11cmRlciB0 cmlhbCANCiAgIGluc3RlYWQgb2YgYW4gYXR0ZW1wdGVkIG11cmRlciB0cmlh bD8NCkE6IFRoZSB2aWN0aW0gbGl2ZWQuDQo= ---827856688-1414219155-1030049918=:7686 Content-Type: APPLICATION/octet-stream; name=arch Content-Transfer-Encoding: BASE64 Content-ID: Content-Description: Content-Disposition: attachment; filename=arch f0VMRgEBAQAAAAAAAAAAAAIAAwABAAAAwIMECDQAAACgCAAAAAAAADQAIAAG ACgAGgAZAAYAAAA0AAAANIAECDSABAjAAAAAwAAAAAUAAAAEAAAAAwAAAPQA AAD0gAQI9IAECBMAAAATAAAABAAAAAEAAAABAAAAAAAAAACABAgAgAQIbQUA AG0FAAAFAAAAABAAAAEAAABwBQAAcJUECHCVBAgUAQAALAEAAAYAAAAAEAAA AgAAAIQFAACElQQIhJUECMgAAADIAAAABgAAAAQAAAAEAAAACAEAAAiBBAgI gQQIIAAAACAAAAAEAAAABAAAAC9saWIvbGQtbGludXguc28uMgAABAAAABAA AAABAAAAR05VAAAAAAACAAAAAAAAAAAAAAADAAAACgAAAAkAAAAFAAAACAAA AAAAAAAAAAAAAAAAAAEAAAACAAAAAwAAAAAAAAAGAAAABwAAAAQAAAAAAAAA AAAAAAAAAAAAAAAAZwAAAFyDBAgqAAAAIgAAABIAAABsgwQI4gAAABIAAAAu AAAAfIMECCUAAAAiAAAAKAAAAIyDBAg6AAAAEgAAAFUAAACcgwQIwwAAABIA AAALAAAArIMECDIAAAASAAAAGQAAAAAAAACGAAAAIgAAAEYAAABghQQIBAAA ABEADgB9AAAAAAAAAAAAAAAgAAAAAGxpYmMuc28uNgBwcmludGYAcGVycm9y AF9fY3hhX2ZpbmFsaXplAHVuYW1lAF9fZGVyZWdpc3Rlcl9mcmFtZV9pbmZv AF9JT19zdGRpbl91c2VkAF9fbGliY19zdGFydF9tYWluAF9fcmVnaXN0ZXJf ZnJhbWVfaW5mbwBfX2dtb25fc3RhcnRfXwBHTElCQ18yLjEuMwBHTElCQ18y LjAAAAACAAIAAgACAAIAAgADAAEAAAAAAAEAAgABAAAAEAAAAAAAAABzH2kJ AAADAIwAAAAQAAAAEGlpDQAAAgCYAAAAAAAAAICWBAgGCQAAaJYECAcBAABs lgQIBwIAAHCWBAgHAwAAdJYECAcEAAB4lgQIBwUAAHyWBAgHBgAAVYnlg+wU U+gAAAAAW4HDLBMAAOioAAAA6C8BAADougEAAFvJwwAAAP81YJYECP8lZJYE CAAAAAD/JWiWBAhoAAAAAOng/////yVslgQIaAgAAADp0P////8lcJYECGgQ AAAA6cD/////JXSWBAhoGAAAAOmw/////yV4lgQIaCAAAADpoP////8lfJYE CGgoAAAA6ZD///8AAAAAMe1eieGD5PBQVFJoQIUECGgkgwQIUVZosIQECOi7 ////9In2VYnlg+wUU+gAAAAAW4HDbBIAAIuDJAAAAIXAdAL/0FvJw4n2kJCQ kJCQkJBVieWD7AiDPXyVBAgAdT7rEqF4lQQIjVAEiRV4lQQIiwD/0KF4lQQI gzgAdeS4fIMECIXAdA2DxPRogJUECOgp////xwV8lQQIAQAAAInsXcONdgBV ieWD7AiJ7F3DifZVieWD7Ai4XIMECIXAdBKDxPhohJYECGiAlQQI6Mv+//+J 7F3DjXYAVYnlg+wIiexdw420JgAAAACNvCcAAAAAVYnlgeyYAQAAg8T0jYV4 /v//UOjE/v//g8QQhcB1GIPE+I2FfP///1BoaYUECOjJ/v//McDrEoPE9Ghk hQQI6Hj+//+4AQAAAInsXcONdgBVieWD7BRTu0yWBAiDPUyWBAj/dAyLA//Q g8P8gzv/dfRbiexdw4n2VYnlg+wIiexdw420JgAAAACNvCcAAAAAVYnlg+wU U+gAAAAAW4HDEBEAAJDot/7//1vJwwMAAAABAAIAYXJjaAAlcwoAAAAAAAAA AAAAAABYlgQIAAAAAAAAAAABAAAAAQAAAAwAAAAkgwQIDQAAAECFBAgEAAAA KIEECAUAAAAEggQIBgAAAGSBBAgKAAAAogAAAAsAAAAQAAAAFQAAAAAAAAAD AAAAXJYECAIAAAAwAAAAFAAAABEAAAAXAAAA9IIECBEAAADsggQIEgAAAAgA AAATAAAACAAAAP7//2+8ggQI////bwEAAADw//9vpoIECAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA /////wAAAACElQQIAAAAAAAAAABigwQIcoMECIKDBAiSgwQIooMECLKDBAgA AAAAAEdDQzogKEdOVSkgMi45NS4zIDIwMDEwMzE1IChyZWxlYXNlKQAAR0ND OiAoR05VKSAyLjk1LjMgMjAwMTAzMTUgKHJlbGVhc2UpAABHQ0M6IChHTlUp IDIuOTUuMyAyMDAxMDMxNSAocmVsZWFzZSkAAEdDQzogKEdOVSkgMi45NS4z IDIwMDEwMzE1IChyZWxlYXNlKQAAR0NDOiAoR05VKSAyLjk1LjMgMjAwMTAz MTUgKHJlbGVhc2UpAABHQ0M6IChHTlUpIDIuOTUuMyAyMDAxMDMxNSAocmVs ZWFzZSkACAAAAAAAAAABAAAAMDEuMDEAAAAIAAAAAAAAAAEAAAAwMS4wMQAA AAgAAAAAAAAAAQAAADAxLjAxAAAACAAAAAAAAAABAAAAMDEuMDEAAAAIAAAA AAAAAAEAAAAwMS4wMQAAAAgAAAAAAAAAAQAAADAxLjAxAAAAAC5zaHN0cnRh YgAuaW50ZXJwAC5ub3RlLkFCSS10YWcALmhhc2gALmR5bnN5bQAuZHluc3Ry AC5nbnUudmVyc2lvbgAuZ251LnZlcnNpb25fcgAucmVsLmdvdAAucmVsLnBs dAAuaW5pdAAudGV4dAAuZmluaQAucm9kYXRhAC5kYXRhAC5laF9mcmFtZQAu ZHluYW1pYwAuY3RvcnMALmR0b3JzAC5zYnNzAC5ic3MALmNvbW1lbnQALm5v dGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsA AAABAAAAAgAAAPSABAj0AAAAEwAAAAAAAAAAAAAAAQAAAAAAAAATAAAABwAA AAIAAAAIgQQICAEAACAAAAAAAAAAAAAAAAQAAAAAAAAAIQAAAAUAAAACAAAA KIEECCgBAAA8AAAABAAAAAAAAAAEAAAABAAAACcAAAALAAAAAgAAAGSBBAhk AQAAoAAAAAUAAAABAAAABAAAABAAAAAvAAAAAwAAAAIAAAAEggQIBAIAAKIA AAAAAAAAAAAAAAEAAAAAAAAANwAAAP///28CAAAApoIECKYCAAAUAAAABAAA AAAAAAACAAAAAgAAAEQAAAD+//9vAgAAALyCBAi8AgAAMAAAAAUAAAABAAAA BAAAAAAAAABTAAAACQAAAAIAAADsggQI7AIAAAgAAAAEAAAAFAAAAAQAAAAI AAAAXAAAAAkAAAACAAAA9IIECPQCAAAwAAAABAAAAAsAAAAEAAAACAAAAGUA AAABAAAABgAAACSDBAgkAwAAJQAAAAAAAAAAAAAABAAAAAAAAABgAAAAAQAA AAYAAABMgwQITAMAAHAAAAAAAAAAAAAAAAQAAAAEAAAAawAAAAEAAAAGAAAA wIMECMADAACAAQAAAAAAAAAAAAAQAAAAAAAAAHEAAAABAAAABgAAAECFBAhA BQAAHAAAAAAAAAAAAAAABAAAAAAAAAB3AAAAAQAAAAIAAABchQQIXAUAABEA AAAAAAAAAAAAAAQAAAAAAAAAfwAAAAEAAAADAAAAcJUECHAFAAAQAAAAAAAA AAAAAAAEAAAAAAAAAIUAAAABAAAAAwAAAICVBAiABQAABAAAAAAAAAAAAAAA BAAAAAAAAACPAAAABgAAAAMAAACElQQIhAUAAMgAAAAFAAAAAAAAAAQAAAAI AAAAmAAAAAEAAAADAAAATJYECEwGAAAIAAAAAAAAAAAAAAAEAAAAAAAAAJ8A AAABAAAAAwAAAFSWBAhUBgAACAAAAAAAAAAAAAAABAAAAAAAAABXAAAAAQAA AAMAAABclgQIXAYAACgAAAAAAAAAAAAAAAQAAAAEAAAApgAAAAEAAAABAAAA hJYECIQGAAAAAAAAAAAAAAAAAAABAAAAAAAAAKwAAAAIAAAAAwAAAISWBAiE BgAAGAAAAAAAAAAAAAAABAAAAAAAAACxAAAAAQAAAAAAAAAAAAAAhAYAAOQA AAAAAAAAAAAAAAEAAAAAAAAAugAAAAcAAAAAAAAAAAAAAGgHAAB4AAAAAAAA AAAAAAABAAAAAAAAAAEAAAADAAAAAAAAAAAAAADgBwAAwAAAAAAAAAAAAAAA AQAAAAAAAAA= ---827856688-1414219155-1030049918=:7686-- pyzor-0.5.0/t/multipart.expected0000644000076500000240000001124711176137136016252 0ustar tameyerstaffThree rules for sounding like an expert: (1) Oversimplify your explanations to the point of uselessness. (2) Always point out second-order effects, but never point out when they can be ignored. (3) Come up with three rules of your own. Dekan's Solace - Introduction

Dekan's Solace

Introduction

I hope your stay in Solace here will let you learn a bit more about me, and that you may find something interesting here.

Other References to Me

My developer account at SourceForge may be of interest; it contains my public diary, which is generally related to development stuff and projects I am working on.

Interesting Sayings

Est Sularus oth Mithas.

One's real life is so often the life that one does not lead. Oscar Wilde (1854-1900), Anglo-Irish playwright, author

To learn what is good and what is to be valued, those truths which cannot be shaken or changed. Myst: The Book of Atrus

Humor in the Court: Q: ...any suggestions as to what prevented this from being a murder trial instead of an attempted murder trial? A: The victim lived. pyzor-0.5.0/THANKS0000644000076500000240000000075711176137136013162 0ustar tameyerstaffPyzor 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 pyzor-0.5.0/unittests.py0000644000076500000240000001466111176137136014662 0ustar tameyerstaffimport sys import unittest import StringIO sys.path.insert(0, './lib') from pyzor import * from pyzor.server import * from pyzor.client import * __revision__ = "$Id: unittests.py,v 1.8 2002-09-07 22:45:03 ftobin Exp $" class ACLTest(unittest.TestCase): def setUp(self): access_file = AccessFile(StringIO.StringIO("""check : alice : allow # comment check : bob : deny report check : all : allow ping : all : deny all : charlie : allow all : all : allow all : all : deny """)) self.acl = ACL() access_file.feed_into(self.acl) def test_basic_allow(self): self.assert_(self.acl.allows(Username('alice'), Opname('check'))) def test_basic_deny(self): self.assert_(not self.acl.allows(Username('bob'), Opname('check'))) def test_all_user_allow(self): self.assert_(self.acl.allows(Username('dennis'), Opname('report'))) self.assert_(self.acl.allows(Username('bob'), Opname('report'))) def test_all_user_deny(self): self.assert_(not self.acl.allows(Username('alice'), Opname('ping'))) self.assert_(not self.acl.allows(Username('frank'), Opname('ping'))) def test_allow_user_all_ops(self): self.assert_(self.acl.allows(Username('charlie'), Opname('check'))) self.assert_(self.acl.allows(Username('charlie'), Opname('foobar'))) def test_all_allowed_all(self): self.assert_(self.acl.allows(Username('giggles'), Opname('report'))) self.assert_(self.acl.allows(Username('zoe'), Opname('foobar'))) class PasswdTest(unittest.TestCase): def setUp(self): passwd_file = PasswdFile(StringIO.StringIO("""alice:5 bob:b charlie:cc """)) self.passwd = Passwd() for u,k in passwd_file: self.passwd[u] = k def test_keys(self): self.assertEquals(self.passwd[Username('alice')], 5L) self.assertEquals(self.passwd[Username('bob')], 11L) self.assertEquals(self.passwd[Username('charlie')], 204L) def test_no_user(self): self.assert_(not self.passwd.has_key(Username('foobar'))) class AcountInfoTest(unittest.TestCase): def setUp(self): account_file = AccountsFile(StringIO.StringIO("""127.0.0.0 : 3333 : alice : 5,a # comment 127.0.0.1 : 4444 : bob : ,18 """)) ## # For testing in the future ## 127.0.0.1 : 4445 : charlie : c, ## 127.0.0.1 : 4446 : david : , self.accounts = AccountsDict() for addr, acc in account_file: self.accounts[addr] = acc def test_full_key(self): self.assertEquals(self.accounts[Address(('127.0.0.0', 3333))], Account((Username('alice'), Keystuff((5L, 10L))))) def test_only_key(self): self.assertEquals(self.accounts[Address(('127.0.0.1', 4444))], Account((Username('bob'), Keystuff((None, 24L))))) ## def test_only_salt(self): ## self.assertEquals(self.accounts[Address(('127.0.0.1', 4445))], ## Account((Username('charlie'), ## Keystuff((None, 12))))) ## def test_neither(self): ## self.assertEquals(self.accounts[Address(('127.0.0.1', 4446))], ## Account((Username('david'), ## Keystuff((None, 24))))) class KeystuffTest(unittest.TestCase): def test_full_stuff(self): self.assertEquals(Keystuff.from_hexstr("10,ab"), Keystuff((16L, 171L))) def test_only_key(self): self.assertEquals(Keystuff.from_hexstr(",ab"), Keystuff((None, 171L))) class ServerListTest(unittest.TestCase): def setUp(self): self.sl = ServerList() self.sl.read(StringIO.StringIO("""127.0.0.1:4444 # comment 127.0.0.2:1234 """)) def test_sl_length(self): self.assertEquals(len(self.sl), 2) def test_entries(self): self.assert_(Address(('127.0.0.1', 4444)) in self.sl) self.assert_(Address(('127.0.0.2', 1234)) in self.sl) class DataDigestTest(unittest.TestCase): def test_ptrns(self): norm = DataDigester.normalize self.assertEqual(norm('aaa me@example.com bbb'), 'aaabbb') self.assertEqual(norm('aaa http://www.example.com/ bbb'), 'aaabbb') self.assertEqual(norm('aaa Supercalifragilisticexpialidocious bbb'), 'aaabbb') self.assertEqual(norm('aaa bbb ccc\n'), 'aaabbbccc') self.assertEqual(norm('aaa bbb'), 'aaabbb') def test_should_handle_line(self): min_len = int(DataDigester.min_line_length) self.assert_(DataDigester.should_handle_line('a' * min_len)) self.assert_(not DataDigester.should_handle_line('a' * (min_len-1))) def test_atomicness(self): self.assert_(DataDigester(StringIO.StringIO(""" It is contrary to reasoning to say that there is a vacuum or space in which there is absolutely nothing. -- Descartes """), ExecCall.digest_spec, seekable=True).is_atomic()) def test_non_atomicness(self): self.assert_(not DataDigester(StringIO.StringIO(""" If builders built buildings the way programmers write programs, Jolt Cola would be a Fortune-500 company. If builders built buildings the way programmers write programs, you'd be able to buy a nice little colonial split-level at Babbages for $34.95. If programmers wrote programs the way builders build buildings, we'd still be using autocoder and running compile decks. --- Love is always open arms. With arms open you allow love to come and go as it wills, freely, for it will do so anyway. If you close your arms about love you'll find you are left only holding yourself. """), ExecCall.digest_spec, seekable=True).is_atomic()) class rfc822BodyCleanerTest(unittest.TestCase): def test_cleaning(self): expected = open('t/multipart.expected') for line in rfc822BodyCleaner(open('t/multipart')): self.assertEqual(line, expected.readline()) if __name__ == "__main__": unittest.main() pyzor-0.5.0/UPGRADING0000644000076500000240000000041611176137136013502 0ustar tameyerstaffIf you are upgrading to Pyzor 0.3.x or newer from an older version, please remove your old ~/.pyzor/servers file, as the protocol is not backwards compatible. The Pyzor client will refreshen the servers file to point to the the public server handling the new protocol.