Imported Upstream version 0.8.4

debian-upstream sdist/0.8.4
Yaroslav Halchenko 2009-09-10 15:08:14 -04:00
parent fec4e7d286
commit c13c64d28b
43 changed files with 235 additions and 80 deletions

View File

@ -4,11 +4,20 @@
|_| \__,_|_|_/___|_.__/\__,_|_||_|
================================================================================
Fail2Ban (version 0.8.4) 2009/02/??
Fail2Ban (version 0.8.4) 2009/09/07
================================================================================
ver. 0.8.4 (2009/??/??) - stable
ver. 0.8.4 (2009/09/07) - stable
----------
- Check the inode number for rotation in addition to checking the first line of
the file. Thanks to Jonathan Kamens. Red Hat #503852. Tracker #2800279.
- Moved the shutdown of the logging subsystem out of Server.quit() to
the end of Server.start(). Fixes the 'cannot release un-acquired lock'
error.
- Added "Ban IP" command. Thanks to Arturo 'Buanzo' Busleiman.
- Added two new filters: lighttpd-fastcgi and php-url-fopen.
- Fixed the 'unexpected communication error' problem by means of
use_poll=False in Python >= 2.6.
- Merged patches from Debian package. Thanks to Yaroslav Halchenko.
- Use current day and month instead of Jan 1st if both are not available in the
log. Thanks to Andreas Itzchak Rehberg.

View File

@ -1,6 +1,6 @@
Metadata-Version: 1.0
Name: fail2ban
Version: 0.8.3
Version: 0.8.4
Summary: Ban IPs that make too many password failure
Home-page: http://www.fail2ban.org
Author: Cyril Jaquier

5
README
View File

@ -4,7 +4,7 @@
|_| \__,_|_|_/___|_.__/\__,_|_||_|
================================================================================
Fail2Ban (version 0.8.4) 2009/??/??
Fail2Ban (version 0.8.4) 2009/09/07
================================================================================
Fail2Ban scans log files like /var/log/pwdfail and bans IP that makes too many
@ -71,7 +71,8 @@ Munger, Christoph Haas, Justin Shore, Joël Bertrand, René Berber, mEDI, Axel
Thimm, Eric Gerbier, Christian Rauch, Michael C. Haller, Jonathan Underwood,
Hanno 'Rince' Wagner, Daniel B. Cid, David Nutter, Raphaël Marichez, Guillaume
Delvit, Vaclav Misek, Adrien Clerc, Michael Hanselmann, Vincent Deffontaines,
Bill Heaton, Russell Odom, Christos Psonis and many others.
Bill Heaton, Russell Odom, Christos Psonis, Arturo 'Buanzo' Busleiman and many
others.
License:
--------

2
TODO
View File

@ -4,7 +4,7 @@
|_| \__,_|_|_/___|_.__/\__,_|_||_|
================================================================================
ToDo $Revision$
ToDo $Revision: 732 $
================================================================================
Legend:

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 505 $
# $Revision: 711 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 505 $"
__date__ = "$Date: 2006-12-24 00:20:16 +0100 (Sun, 24 Dec 2006) $"
__version__ = "$Revision: 711 $"
__date__ = "$Date: 2008-08-13 00:05:13 +0200 (Wed, 13 Aug 2008) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

38
common/helpers.py Normal file
View File

@ -0,0 +1,38 @@
# This file is part of Fail2Ban.
#
# Fail2Ban 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.
#
# Fail2Ban 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 Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# Author: Cyril Jaquier
# Author: Arturo 'Buanzo' Busleiman
#
# $Revision: 741 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 741 $"
__date__ = "$Date: 2009-08-30 16:13:04 +0200 (Sun, 30 Aug 2009) $"
__copyright__ = "Copyright (c) 2009 Cyril Jaquier"
__license__ = "GPL"
def formatExceptionInfo():
""" Author: Arturo 'Buanzo' Busleiman """
import sys
cla, exc = sys.exc_info()[:2]
excName = cla.__name__
try:
excArgs = exc.__dict__["args"]
except KeyError:
excArgs = str(exc)
return (excName, excArgs)

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 662 $
# $Revision: 751 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 662 $"
__date__ = "$Date: 2008-03-05 00:41:58 +0100 (Wed, 05 Mar 2008) $"
__version__ = "$Revision: 751 $"
__date__ = "$Date: 2009-09-01 22:25:32 +0200 (Tue, 01 Sep 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
@ -52,13 +52,14 @@ protocol = [
["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"],
["set <JAIL> delignoreip <IP>", "removes <IP> from the ignore list of <JAIL>"],
["set <JAIL> addlogpath <FILE>", "adds <FILE> to the monitoring list of <JAIL>"],
["set <JAIL> dellogpath <FILE>", "removes <FILE> to the monitoring list of <JAIL>"],
["set <JAIL> dellogpath <FILE>", "removes <FILE> from the monitoring list of <JAIL>"],
["set <JAIL> addfailregex <REGEX>", "adds the regular expression <REGEX> which must match failures for <JAIL>"],
["set <JAIL> delfailregex <INDEX>", "removes the regular expression at <INDEX> for failregex"],
["set <JAIL> addignoreregex <REGEX>", "adds the regular expression <REGEX> which should match pattern to exclude for <JAIL>"],
["set <JAIL> delignoreregex <INDEX>", "removes the regular expression at <INDEX> for ignoreregex"],
["set <JAIL> findtime <TIME>", "sets the number of seconds <TIME> for which the filter will look back for <JAIL>"],
["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"],
["set <JAIL> banip <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> addaction <ACT>", "adds a new action named <NAME> for <JAIL>"],
["set <JAIL> delaction <ACT>", "removes the action <NAME> from <JAIL>"],

View File

@ -16,12 +16,12 @@
# Author: Cyril Jaquier
#
# $Revision: 703 $
# $Revision: 754 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 703 $"
__date__ = "$Date: 2008-07-17 23:28:51 +0200 (Thu, 17 Jul 2008) $"
__version__ = "$Revision: 754 $"
__date__ = "$Date: 2009-09-07 21:13:45 +0200 (Mon, 07 Sep 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
version = "0.8.3-SVN"
version = "0.8.4"

View File

@ -12,7 +12,7 @@
# any other addresses found in the whois record, with a few exceptions.
# If no addresses are found, no e-mail is sent.
#
# $Revision$
# $Revision: 717 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 569 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Yaroslav O. Halchenko <debian@onerussian.com>
#
# $Revision$
# $Revision: 716 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 658 $
# $Revision: 728 $
#
[Definition]

View File

@ -3,7 +3,7 @@
#
# Author: Yaroslav Halchenko
#
# $Revision$
# $Revision: 728 $
#
[INCLUDES]

View File

@ -3,7 +3,7 @@
# Author: Christoph Haas
# Modified by: Cyril Jaquier
#
# $Revision: 510 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 510 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Jan Wagner <waja@cyconet.org>
#
# $Revision$
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 510 $
# $Revision: 728 $
#
[Definition]

View File

@ -0,0 +1,18 @@
# Fail2Ban configuration file
#
# Author: Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
#
[Definition]
# Option: failregex
# Notes.: regex to match ALERTS as notified by lighttpd's FastCGI Module
# Values: TEXT
#
failregex = .*ALERT\ -\ .*attacker\ \'<HOST>\'
# Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored.
# Values: TEXT
#
ignoreregex =

View File

@ -4,7 +4,7 @@
#
# Author: Yaroslav Halchenko
#
# $Revision: 699 $
# $Revision: 730 $
#
[Definition]

View File

@ -0,0 +1,23 @@
# Fail2Ban configuration file
#
# Author: Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
# Version 2
# fixes the failregex so REFERERS that contain =http:// don't get blocked
# (mentioned by "fasuto" (no real email provided... blog comment) in this entry:
# http://blogs.buanzo.com.ar/2009/04/fail2ban-filter-for-php-injection-attacks.html#comment-1489
#
[Definition]
# Option: failregex
# Notes.: regex to match this kind of request:
#
# 66.185.212.172 - - [26/Mar/2009:08:44:20 -0500] "GET /index.php?n=http://eatmyfood.hostinginfive.com/pizza.htm? HTTP/1.1" 200 114 "-" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"
#
failregex = ^<HOST> -.*"(GET|POST).*\?.*\=http\:\/\/.* HTTP\/.*$
# Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored.
# Values: TEXT
#
ignoreregex =

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 510 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Yaroslav Halchenko
#
# $Revision: 677 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 510 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Yaroslav Halchenko
#
# $Revision: 510 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Jan Wagner <waja@cyconet.org>
#
# $Revision$
# $Revision: 727 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Yaroslav Halchenko
#
# $Revision: 592 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 663 $
# $Revision: 728 $
#
[INCLUDES]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 658 $
# $Revision: 728 $
#
[Definition]

View File

@ -3,7 +3,7 @@
# Author: Cyril Jaquier
# Rule by : Delvit Guillaume
#
# $Revision: 601 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Guido Bozzetto
#
# $Revision: 668 $
# $Revision: 728 $
#
[Definition]

View File

@ -2,7 +2,7 @@
#
# Author: Cyril Jaquier
#
# $Revision: 617 $
# $Revision: 747 $
#
# The DEFAULT allows a global definition of the options. They can be override
@ -152,6 +152,34 @@ action = shorewall
sendmail[name=Postfix, dest=you@mail.com]
logpath = /var/log/apache2/error_log
# Ban attackers that try to use PHP's URL-fopen() functionality
# through GET/POST variables. - Experimental, with more than a year
# of usage in production environments.
[php-url-fopen]
enabled = false
port = http,https
filter = php-url-fopen
logpath = /var/www/*/logs/access_log
maxretry = 1
# A simple PHP-fastcgi jail which works with lighttpd.
# If you run a lighttpd server, then you probably will
# find these kinds of messages in your error_log:
# ALERT tried to register forbidden variable GLOBALS
# through GET variables (attacker '1.2.3.4', file '/var/www/default/htdocs/index.php')
# This jail would block the IP 1.2.3.4.
[lighttpd-fastcgi]
enabled = false
port = http,https
filter = lighttpd-fastcgi
# adapt the following two items as needed
logpath = /var/log/lighttpd/error.log
maxretry = 2
# This jail uses ipfw, the standard firewall on FreeBSD. The "ignoreip"
# option is overridden in this jail. Moreover, the action "mail-whois" defines
# the variable "name" which contains a comma using "". The characters '' are

View File

@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
from pickle import dumps, loads, HIGHEST_PROTOCOL
import asyncore, asynchat, socket, os, logging
from common import helpers
import asyncore, asynchat, socket, os, logging, sys, traceback
# Gets the instance of the logger.
logSys = logging.getLogger("fail2ban.server")
@ -69,7 +70,9 @@ class RequestHandler(asynchat.async_chat):
self.close_when_done()
def handle_error(self):
logSys.error("Unexpected communication error")
e1,e2 = helpers.formatExceptionInfo()
logSys.error("Unexpected communication error: "+e2)
logSys.error(traceback.format_exc().splitlines())
self.close()
##
@ -132,6 +135,12 @@ class AsyncServer(asyncore.dispatcher):
# Sets the init flag.
self.__init = True
# TODO Add try..catch
# There's a bug report for Python 2.6/3.0 that use_poll=True yields some 2.5 incompatibilities:
if sys.version_info >= (2, 6): # if python 2.6 or greater...
logSys.debug("Detected Python 2.6 or greater. asyncore.loop() not using poll")
asyncore.loop(use_poll = False) # fixes the "Unexpected communication problem" issue on Python 2.6 and 3.0
else:
logSys.debug("NOT Python 2.6/3.* - asyncore.loop() using poll")
asyncore.loop(use_poll = True)
##

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 692 $
# $Revision: 722 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 692 $"
__date__ = "$Date: 2008-05-18 21:53:18 +0200 (Sun, 18 May 2008) $"
__version__ = "$Revision: 722 $"
__date__ = "$Date: 2009-01-28 00:21:55 +0100 (Wed, 28 Jan 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

View File

@ -17,11 +17,11 @@
# Author: Cyril Jaquier
#
# $Revision: 692 $
# $Revision: 729 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 692 $"
__date__ = "$Date: 2008-05-18 21:53:18 +0200 (Sun, 18 May 2008) $"
__version__ = "$Revision: 729 $"
__date__ = "$Date: 2009-02-08 20:50:44 +0100 (Sun, 08 Feb 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 382 $
# $Revision: 731 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 382 $"
__date__ = "$Date: 2006-09-25 19:03:48 +0200 (Mon, 25 Sep 2006) $"
__version__ = "$Revision: 731 $"
__date__ = "$Date: 2009-02-09 23:08:21 +0100 (Mon, 09 Feb 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 638 $
# $Revision: 731 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 638 $"
__date__ = "$Date: 2007-12-17 21:00:36 +0100 (Mon, 17 Dec 2007) $"
__version__ = "$Revision: 731 $"
__date__ = "$Date: 2009-02-09 23:08:21 +0100 (Mon, 09 Feb 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 642 $
# $Revision: 728 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 642 $"
__date__ = "$Date: 2008-01-05 23:33:44 +0100 (Sat, 05 Jan 2008) $"
__version__ = "$Revision: 728 $"
__date__ = "$Date: 2009-02-08 18:31:24 +0100 (Sun, 08 Feb 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 696 $
# $Revision: 752 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 696 $"
__date__ = "$Date: 2008-05-19 23:05:32 +0200 (Mon, 19 May 2008) $"
__version__ = "$Revision: 752 $"
__date__ = "$Date: 2009-09-01 23:21:30 +0200 (Tue, 01 Sep 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
@ -31,7 +31,7 @@ from datedetector import DateDetector
from mytime import MyTime
from failregex import FailRegex, Regex, RegexException
import logging, re
import logging, re, os
# Gets the instance of the logger.
logSys = logging.getLogger("fail2ban.filter")
@ -183,6 +183,17 @@ class Filter(JailThread):
def run(self):
raise Exception("run() is abstract")
##
# Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html
# Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
#
# to enable banip fail2ban-client BAN command
def addBannedIP(self, ip):
unixTime = time.time()
self.failManager.addFailure(FailTicket(ip, unixTime))
return ip
##
# Add an IP/DNS to the ignore list.
#
@ -438,6 +449,8 @@ class FileContainer:
self.__handler = None
# Try to open the file. Raises an exception if an error occured.
handler = open(filename)
stats = os.fstat(handler.fileno())
self.__ino = stats.st_ino
try:
firstLine = handler.readline()
# Computes the MD5 of the first line.
@ -459,10 +472,12 @@ class FileContainer:
firstLine = self.__handler.readline()
# Computes the MD5 of the first line.
myHash = md5.new(firstLine).digest()
# Compare hash.
if not self.__hash == myHash:
stats = os.fstat(self.__handler.fileno())
# Compare hash and inode
if self.__hash != myHash or self.__ino != stats.st_ino:
logSys.info("Log rotation detected for %s" % self.__filename)
self.__hash = myHash
self.__ino = stats.st_ino
self.__pos = 0
# Sets the file pointer to the last position.
self.__handler.seek(self.__pos)

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 696 $
# $Revision: 748 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 696 $"
__date__ = "$Date: 2008-05-19 23:05:32 +0200 (Mon, 19 May 2008) $"
__version__ = "$Revision: 748 $"
__date__ = "$Date: 2009-08-31 16:14:02 +0200 (Mon, 31 Aug 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
@ -96,11 +96,6 @@ class Server:
except OSError, e:
logSys.error("Unable to remove PID file: %s" % e)
logSys.info("Exiting Fail2ban")
def quit(self):
self.stopAllJail()
# Stop communication
self.__asyncServer.stop()
# Shutdowns the logging.
try:
self.__loggingLock.acquire()
@ -108,6 +103,11 @@ class Server:
finally:
self.__loggingLock.release()
def quit(self):
self.stopAllJail()
# Stop communication
self.__asyncServer.stop()
def addJail(self, name, backend):
self.__jails.add(name, backend)
@ -160,7 +160,7 @@ class Server:
return self.__jails.getFilter(name).getIgnoreIP()
def addLogPath(self, name, fileName):
self.__jails.getFilter(name).addLogPath(fileName)
self.__jails.getFilter(name).addLogPath(fileName, True)
def delLogPath(self, name, fileName):
self.__jails.getFilter(name).delLogPath(fileName)
@ -221,6 +221,9 @@ class Server:
def setBanTime(self, name, value):
self.__jails.getAction(name).setBanTime(value)
def setBanIP(self, name, value):
return self.__jails.getFilter(name).addBannedIP(value)
def getBanTime(self, name):
return self.__jails.getAction(name).getBanTime()

View File

@ -35,6 +35,7 @@ class Ticket:
self.__ip = ip
self.__time = time
self.__attempt = 0
self.__file = None
def setIP(self, value):
self.__ip = value
@ -42,6 +43,12 @@ class Ticket:
def getIP(self):
return self.__ip
def setFile(self, value):
self.__file = value
def getFile(self):
return self.__file
def setTime(self, value):
self.__time = value

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 639 $
# $Revision: 745 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 639 $"
__date__ = "$Date: 2007-12-17 21:04:29 +0100 (Mon, 17 Dec 2007) $"
__version__ = "$Revision: 745 $"
__date__ = "$Date: 2009-08-30 20:26:15 +0200 (Sun, 30 Aug 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
@ -164,6 +164,9 @@ class Transmitter:
value = command[2]
self.__server.setBanTime(name, int(value))
return self.__server.getBanTime(name)
elif command[1] == "banip":
value = command[2]
return self.__server.setBanIP(name,value)
elif command[1] == "addaction":
value = command[2]
self.__server.addAction(name, value)

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 638 $
# $Revision: 731 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 638 $"
__date__ = "$Date: 2007-12-17 21:00:36 +0100 (Mon, 17 Dec 2007) $"
__version__ = "$Revision: 731 $"
__date__ = "$Date: 2009-02-09 23:08:21 +0100 (Mon, 09 Feb 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

View File

@ -16,11 +16,11 @@
# Author: Cyril Jaquier
#
# $Revision: 641 $
# $Revision: 728 $
__author__ = "Cyril Jaquier"
__version__ = "$Revision: 641 $"
__date__ = "$Date: 2007-12-26 12:46:22 +0100 (Wed, 26 Dec 2007) $"
__version__ = "$Revision: 728 $"
__date__ = "$Date: 2009-02-08 18:31:24 +0100 (Sun, 08 Feb 2009) $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"