Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43f09a3422 | ||
|
|
111471c777 | ||
|
|
a63b834c8c | ||
|
|
51fc3d7642 | ||
|
|
da568d73fa | ||
|
|
9deafe6355 | ||
|
|
4bd929ffa8 | ||
|
|
254b57e43b | ||
|
|
f39c778a97 | ||
|
|
094b0e651a | ||
|
|
09b2016c05 | ||
|
|
3a1a1f3ad7 | ||
|
|
5edc60934d | ||
|
|
4fea9dde8d | ||
|
|
c82eb16f3d | ||
|
|
dbf6cff032 | ||
|
|
30bc782b18 | ||
|
|
b89222b1d1 | ||
|
|
2874399fed | ||
|
|
5bafc61e48 | ||
|
|
d7087412bb | ||
|
|
c5a754411d | ||
|
|
8faff41d40 | ||
|
|
6eb67899a5 | ||
|
|
dfc7dd6dc9 | ||
|
|
e19f9e1697 | ||
|
|
cb9cbd754e | ||
|
|
f206c3d493 | ||
|
|
8cb6ec54ac | ||
|
|
a45467ac91 | ||
|
|
5c2730d7be | ||
|
|
7260403fdd | ||
|
|
2a31d45582 | ||
|
|
fdcded262d |
+5
-2
@@ -4,6 +4,9 @@ language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
env:
|
||||
- LOG_LEVEL=error
|
||||
- LOG_LEVEL=heavydebug
|
||||
before_install:
|
||||
- sudo apt-get update -qq
|
||||
install:
|
||||
@@ -12,6 +15,6 @@ install:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cd ..; pip install -q coveralls; cd -; fi
|
||||
script:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then export PYTHONPATH="$PYTHONPATH:/usr/share/pyshared:/usr/lib/pyshared/python2.7"; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coverage run --rcfile=.travis_coveragerc fail2ban-testcases; else python ./fail2ban-testcases; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]] && [ $LOG_LEVEL == heavydebug ]; then coverage run --rcfile=.travis_coveragerc fail2ban-testcases -l $LOG_LEVEL; else python ./fail2ban-testcases -l $LOG_LEVEL; fi
|
||||
after_success:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coveralls; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]] && [ $LOG_LEVEL == heavydebug ]; then coveralls; fi
|
||||
|
||||
@@ -4,14 +4,32 @@
|
||||
|_| \__,_|_|_/___|_.__/\__,_|_||_|
|
||||
|
||||
================================================================================
|
||||
Fail2Ban (version 0.8.13.dev) 2014/??/??
|
||||
Fail2Ban (version 0.8.14.dev) 2014/../..
|
||||
================================================================================
|
||||
|
||||
ver. 0.8.14 (2014/??/??) - take-care-of-the-elderly
|
||||
ver. 0.8.15 (2014/XX/XXX) - hopefully-never-to-be-released
|
||||
-----------
|
||||
|
||||
- Fixes:
|
||||
|
||||
- New Features:
|
||||
|
||||
- Enhancements:
|
||||
* Date detector learnt Spanish month names (Thanks David AKA darues@github)
|
||||
|
||||
|
||||
ver. 0.8.14 (2014/08/19) - take-care-of-the-elderly
|
||||
-----------
|
||||
|
||||
- Fixes:
|
||||
- minor fixes for claimed Python 2.4 and 2.5 compatibility
|
||||
- Handle case when inotify watch is auto deleted on file deletion to stop
|
||||
error messages
|
||||
- tests - fixed few "leaky" file descriptors when files were not closed while
|
||||
being removed physically
|
||||
- grep in mail*-whois-lines.conf now also matches end of line to work
|
||||
with the recidive filter
|
||||
- filters.d/sieve.conf - fixed typo in _daemon. Thanks Jisoo Park
|
||||
|
||||
|
||||
ver. 0.8.13 (2014/03/15) - maintenance-only-from-now-on
|
||||
|
||||
@@ -316,23 +316,23 @@ Look for errors like:
|
||||
|
||||
Which indicates that testcases/files/logs/mysqld.log has been moved or is a directory
|
||||
|
||||
tar -C /tmp -jxf dist/fail2ban-0.8.14.tar.bz2
|
||||
tar -C /tmp -jxf dist/fail2ban-0.8.15.tar.bz2
|
||||
|
||||
# clean up current direcory
|
||||
|
||||
diff -rul --exclude \*.pyc . /tmp/fail2ban-0.8.14/
|
||||
diff -rul --exclude \*.pyc . /tmp/fail2ban-0.8.15/
|
||||
|
||||
# Only differences should be files that you don't want distributed.
|
||||
|
||||
# Ensure the tests work from the tarball
|
||||
|
||||
cd /tmp/fail2ban-0.8.14/ && ./fail2ban-testcases-all
|
||||
cd /tmp/fail2ban-0.8.15/ && ./fail2ban-testcases-all
|
||||
|
||||
# Add/finalize the corresponding entry in the ChangeLog
|
||||
|
||||
To generate a list of committers use e.g.
|
||||
|
||||
git shortlog -sn 0.8.13.. | sed -e 's,^[ 0-9\t]*,,g' | tr '\n' '\|' | sed -e 's:|:, :g'
|
||||
git shortlog -sn 0.8.14.. | sed -e 's,^[ 0-9\t]*,,g' | tr '\n' '\|' | sed -e 's:|:, :g'
|
||||
|
||||
Ensure the top of the ChangeLog has the right version and current date.
|
||||
|
||||
@@ -409,7 +409,7 @@ Post Release
|
||||
|
||||
Add the following to the top of the ChangeLog
|
||||
|
||||
ver. 0.8.13 (2014/XX/XXX) - wanna-be-released
|
||||
ver. 0.8.15 (2014/XX/XXX) - wanna-be-released
|
||||
-----------
|
||||
|
||||
- Fixes:
|
||||
|
||||
@@ -465,5 +465,3 @@ log message with a <USER> of 123.1.1.1.
|
||||
|
||||
Here there's nothing to do except request/change the application so it logs
|
||||
messages differently.
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -6,20 +6,20 @@ By Roy Sigurd Karlsbakk <roy@karlsbakk.net>
|
||||
|
||||
ABOUT
|
||||
|
||||
This readme is meant for those wanting to install fail2ban on Solaris 10,
|
||||
This README is meant for those wanting to install fail2ban on Solaris 10,
|
||||
OpenSolaris, OpenIndiana etc. To some degree it may as well be useful for
|
||||
users of older Solaris versions and Nexenta, but don't rely on it.
|
||||
|
||||
READ ME FIRST
|
||||
|
||||
If I use the term Solaris, I am talking about any Solaris dialect, that is, the
|
||||
official Sun/Oracle ones or derivates. If I describe an OS as
|
||||
official Sun/Oracle ones or derivatives. If I describe an OS as
|
||||
"OpenSolaris-based", it means it's either OpenSolaris, OpenIndiana or one of the
|
||||
other, but /not/ the Nexenta family, since this only uses the OpenSolaris/
|
||||
IllumOS kernel and not the userland. If I say Solaris 10, I mean Solaris 10 and
|
||||
perhaps, if you're lucky and have some good gods on your side, it may also apply
|
||||
to Solaris 9 or even 8 and hopefully in the new Solaris 11 whenever that may be
|
||||
released. Quoted lines of code, settings et cetera are indented with two spaces.
|
||||
released. Quoted lines of code, settings etc. are indented with two spaces.
|
||||
This does _not_ mean you should use that indentation, especially in config files
|
||||
where they can be harmful. Optional settings are prefixed with OPT: while
|
||||
required settings are prefixed with REQ:. If no prefix is found, regard it as a
|
||||
@@ -111,7 +111,7 @@ GOTCHAS AND FIXMES
|
||||
svcadm enable fail2ban
|
||||
|
||||
* If svcs -xv says that fail2ban failed to start or svcs says it's in maintenance mode
|
||||
check /var/svc/log/network-fail2ban:default.log for clues.
|
||||
check /var/svc/log/network-fail2ban:default.log for clues.
|
||||
Check permissions on /var/adm, /var/adm/auth.log /var/adm/fail2ban.log and /var/run/fail2ban
|
||||
You may need to:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/ _|__ _(_) |_ ) |__ __ _ _ _
|
||||
| _/ _` | | |/ /| '_ \/ _` | ' \
|
||||
|_| \__,_|_|_/___|_.__/\__,_|_||_|
|
||||
v0.8.12 2014/01/22
|
||||
v0.8.14.dev 2014/../..
|
||||
|
||||
## Fail2Ban: ban hosts that cause multiple authentication errors
|
||||
|
||||
@@ -30,8 +30,8 @@ Optional:
|
||||
|
||||
To install, just do:
|
||||
|
||||
tar xvfj fail2ban-0.8.12.tar.bz2
|
||||
cd fail2ban-0.8.12
|
||||
tar xvfj fail2ban-0.8.14.tar.bz2
|
||||
cd fail2ban-0.8.14
|
||||
python setup.py install
|
||||
|
||||
This will install Fail2Ban into /usr/share/fail2ban. The executable scripts are
|
||||
|
||||
@@ -45,6 +45,7 @@ Iain Lea
|
||||
Ivo Truxa
|
||||
Jacques Lav!gnotte
|
||||
Ioan Indreias
|
||||
Jisoo Park
|
||||
Jonathan Kamens
|
||||
Jonathan Lanning
|
||||
Jonathan Underwood
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
ToDo
|
||||
================================================================================
|
||||
|
||||
See https://github.com/fail2ban/fail2ban/issues?state=open for the list of
|
||||
current outstanding issues and wishlists. They should also be sorted among
|
||||
milestones and tagged for filtering. This file reflects possibly outdated
|
||||
plans but is kept for someone to move them into github's issues.
|
||||
|
||||
Legend:
|
||||
- not yet done
|
||||
? maybe
|
||||
# partially done
|
||||
* done
|
||||
|
||||
- more detailed explaination in DEVELOP for new developers (eg. howto build this HEX numbers in ChangeLog)
|
||||
|
||||
- Run tests though all filters/examples files - (see sshd example file) as unit
|
||||
test
|
||||
|
||||
* Removed relative imports
|
||||
|
||||
* Cleanup fail2ban-client and fail2ban-server. Move code to server/ and client/
|
||||
|
||||
+1
-1
@@ -24,4 +24,4 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
version = "0.8.13"
|
||||
version = "0.8.14.dev"
|
||||
|
||||
@@ -42,7 +42,7 @@ actionban = printf %%b "Hi,\n
|
||||
Here is more information about <ip>:\n
|
||||
`whois <ip> || echo missing whois program`\n\n
|
||||
Lines containing IP:<ip> in <logpath>\n
|
||||
`grep '[^0-9]<ip>[^0-9]' <logpath>`\n\n
|
||||
`grep -E '(^|[^0-9])<ip>([^0-9]|$)' <logpath>`\n\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from `uname -n`" <dest>
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from `uname -n`
|
||||
Here is more information about <ip>:\n
|
||||
`/usr/bin/whois <ip> || echo missing whois program`\n\n
|
||||
Lines containing IP:<ip> in <logpath>\n
|
||||
`grep '[^0-9]<ip>[^0-9]' <logpath>`\n\n
|
||||
`grep -E '(^|[^0-9])<ip>([^0-9]|$)' <logpath>`\n\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
# connections. So if the attempter goes on trying using the same connection
|
||||
# he could even log in. In order to get the same behavior of the iptable
|
||||
# action (so that the ban is immediate) the /etc/shorewall/shorewall.conf
|
||||
# file should me modified with "BLACKLISTNEWONLY=No".
|
||||
# file should me modified with "BLACKLISTNEWONLY=No". Note that as of
|
||||
# Shorewall 4.5.13 BLACKLISTNEWONLY is deprecated; however the equivalent
|
||||
# of BLACKLISTNEWONLY=No can now be achieved by setting BLACKLIST="ALL".
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
@@ -17,3 +17,5 @@
|
||||
# Values: TEXT
|
||||
#
|
||||
failregex = ^(?:\.\d+)? \[info\] <0\.\d+\.\d>@ejabberd_c2s:wait_for_feature_request:\d+ \([^\)]+\) Failed authentication for \S+ from IP <HOST>$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
failregex = ^\[\]LOGIN FAILED for user: "\S+" from IP: <HOST>$
|
||||
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Author: Daniel Black
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ failregex = ^%(__line_prefix)s(\.\d+)?( error:)?\s*client <HOST>#\S+( \([\S.]+\)
|
||||
^%(__line_prefix)s(\.\d+)?( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: zone transfer '\S+/AXFR/\w+' denied\s*$
|
||||
^%(__line_prefix)s(\.\d+)?( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: bad zone transfer request: '\S+/IN': non-authoritative zone \(NOTAUTH\)\s*$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# DEV Notes:
|
||||
# Trying to generalize the
|
||||
# structure which is general to capture general patterns in log
|
||||
|
||||
@@ -24,3 +24,5 @@ _daemon = nsd
|
||||
|
||||
failregex = ^\[\]%(__prefix_line)sinfo: ratelimit block .* query <HOST> TYPE255$
|
||||
^\[\]%(__prefix_line)sinfo: .* <HOST> refused, no acl matches\.$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
@@ -11,4 +11,6 @@ _daemon = postfix/smtpd
|
||||
|
||||
failregex = ^%(__prefix_line)swarning: [-._\w]+\[<HOST>\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/]*={0,2})?\s*$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Author: Yaroslav Halchenko
|
||||
|
||||
@@ -29,4 +29,6 @@ _jailname = recidive
|
||||
|
||||
failregex = ^(%(__prefix_line)s|,\d{3} fail2ban.actions%(__pid_re)s?:\s+)WARNING\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Author: Tom Hendrikx, modifications by Amir Caspi
|
||||
|
||||
@@ -9,7 +9,7 @@ before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
_deamon = (?:cyrus/)?(?:tim)?sieved?
|
||||
_daemon = (?:cyrus/)?(?:tim)?sieved?
|
||||
|
||||
failregex = ^%(__prefix_line)sbadlogin: \S+ ?\[<HOST>\] \S+ authentication failure$
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
failregex = ^\s+\d\s<HOST>\s+[A-Z_]+_DENIED/403 .*$
|
||||
^\s+\d\s<HOST>\s+NONE/405 .*$
|
||||
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Author: Daniel Black
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.41.2.
|
||||
.TH FAIL2BAN-CLIENT "1" "March 2014" "fail2ban-client v0.8.13" "User Commands"
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.45.1.
|
||||
.TH FAIL2BAN-CLIENT "1" "August 2014" "fail2ban-client v0.8.14" "User Commands"
|
||||
.SH NAME
|
||||
fail2ban-client \- configure and control the server
|
||||
.SH SYNOPSIS
|
||||
.B fail2ban-client
|
||||
[\fIOPTIONS\fR] \fI<COMMAND>\fR
|
||||
[\fI\,OPTIONS\/\fR] \fI\,<COMMAND>\/\fR
|
||||
.SH DESCRIPTION
|
||||
Fail2Ban v0.8.13 reads log file that contains password failure report
|
||||
Fail2Ban v0.8.14 reads log file that contains password failure report
|
||||
and bans the corresponding IP addresses using firewall rules.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.41.2.
|
||||
.TH FAIL2BAN-REGEX "1" "March 2014" "fail2ban-regex 0.8.13" "User Commands"
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.45.1.
|
||||
.TH FAIL2BAN-REGEX "1" "August 2014" "fail2ban-regex 0.8.14" "User Commands"
|
||||
.SH NAME
|
||||
fail2ban-regex \- test Fail2ban "failregex" option
|
||||
.SH SYNOPSIS
|
||||
.B fail2ban-regex
|
||||
[\fIOPTIONS\fR] \fI<LOG> <REGEX> \fR[\fIIGNOREREGEX\fR]
|
||||
[\fI\,OPTIONS\/\fR] \fI\,<LOG> <REGEX> \/\fR[\fI\,IGNOREREGEX\/\fR]
|
||||
.SH DESCRIPTION
|
||||
Fail2Ban reads log file that contains password failure report
|
||||
and bans the corresponding IP addresses using firewall rules.
|
||||
@@ -16,7 +16,7 @@ string
|
||||
a string representing a log line
|
||||
.TP
|
||||
filename
|
||||
path to a log file (\fI/var/log/auth.log\fP)
|
||||
path to a log file (\fI\,/var/log/auth.log\/\fP)
|
||||
.SS "REGEX:"
|
||||
.TP
|
||||
string
|
||||
@@ -39,7 +39,7 @@ show program's version number and exit
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-l\fR LOG_LEVEL, \fB\-\-log\-level\fR=\fILOG_LEVEL\fR
|
||||
\fB\-l\fR LOG_LEVEL, \fB\-\-log\-level\fR=\fI\,LOG_LEVEL\/\fR
|
||||
Log level for the Fail2Ban logger to use
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.41.2.
|
||||
.TH FAIL2BAN-SERVER "1" "March 2014" "fail2ban-server v0.8.13" "User Commands"
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.45.1.
|
||||
.TH FAIL2BAN-SERVER "1" "August 2014" "fail2ban-server v0.8.14" "User Commands"
|
||||
.SH NAME
|
||||
fail2ban-server \- start the server
|
||||
.SH SYNOPSIS
|
||||
.B fail2ban-server
|
||||
[\fIOPTIONS\fR]
|
||||
[\fI\,OPTIONS\/\fR]
|
||||
.SH DESCRIPTION
|
||||
Fail2Ban v0.8.13 reads log file that contains password failure report
|
||||
Fail2Ban v0.8.14 reads log file that contains password failure report
|
||||
and bans the corresponding IP addresses using firewall rules.
|
||||
.PP
|
||||
Only use this command for debugging purpose. Start the server with
|
||||
|
||||
+12
-12
@@ -97,18 +97,18 @@ class DateEpoch(DateTemplate):
|
||||
class DateStrptime(DateTemplate):
|
||||
|
||||
TABLE = dict()
|
||||
TABLE["Jan"] = ["Sty"]
|
||||
TABLE["Feb"] = [u"Fév", "Lut"]
|
||||
TABLE["Mar"] = [u"Mär", "Mar"]
|
||||
TABLE["Apr"] = ["Avr", "Kwi"]
|
||||
TABLE["May"] = ["Mai", "Maj"]
|
||||
TABLE["Jun"] = ["Lip"]
|
||||
TABLE["Jul"] = ["Sie"]
|
||||
TABLE["Aug"] = ["Aou", "Wrz"]
|
||||
TABLE["Sep"] = ["Sie"]
|
||||
TABLE["Oct"] = [u"Paź"]
|
||||
TABLE["Nov"] = ["Lis"]
|
||||
TABLE["Dec"] = [u"Déc", "Dez", "Gru"]
|
||||
TABLE["Jan"] = ["Sty", "ene"]
|
||||
TABLE["Feb"] = [u"Fév", "Lut", "feb"]
|
||||
TABLE["Mar"] = [u"Mär", "Mar", "mar"]
|
||||
TABLE["Apr"] = ["Avr", "Kwi", "abr"]
|
||||
TABLE["May"] = ["Mai", "Maj", "may"]
|
||||
TABLE["Jun"] = ["Lip", "jun"]
|
||||
TABLE["Jul"] = ["Sie", "jul"]
|
||||
TABLE["Aug"] = ["Aou", "Wrz", "ago"]
|
||||
TABLE["Sep"] = ["Sie", "sep"]
|
||||
TABLE["Oct"] = [u"Paź", "oct"]
|
||||
TABLE["Nov"] = ["Lis", "nov"]
|
||||
TABLE["Dec"] = [u"Déc", "Dez", "Gru", "dic"]
|
||||
|
||||
def __init__(self):
|
||||
DateTemplate.__init__(self)
|
||||
|
||||
@@ -125,7 +125,7 @@ class FilterGamin(FileFilter):
|
||||
self.__cleanup()
|
||||
|
||||
##
|
||||
# Desallocates the resources used by Gamin.
|
||||
# Free up the resources used by Gamin.
|
||||
|
||||
def __cleanup(self):
|
||||
for path in self.getLogPath():
|
||||
|
||||
@@ -69,6 +69,7 @@ class FilterPyinotify(FileFilter):
|
||||
# Pyinotify watch manager
|
||||
self.__monitor = pyinotify.WatchManager()
|
||||
self.__watches = dict()
|
||||
self.__notifier = None
|
||||
logSys.debug("Created FilterPyinotify")
|
||||
|
||||
|
||||
@@ -90,6 +91,9 @@ class FilterPyinotify(FileFilter):
|
||||
self._delFileWatcher(path)
|
||||
# place a new one
|
||||
self._addFileWatcher(path)
|
||||
elif event.mask & pyinotify.IN_IGNORED:
|
||||
# Caused when watch removed, either by rm_watch or file deletion
|
||||
return
|
||||
|
||||
self._process_file(path)
|
||||
|
||||
@@ -118,9 +122,11 @@ class FilterPyinotify(FileFilter):
|
||||
|
||||
def _delFileWatcher(self, path):
|
||||
wdInt = self.__watches[path]
|
||||
wd = self.__monitor.rm_watch(wdInt)
|
||||
if wd[wdInt]:
|
||||
del self.__watches[path]
|
||||
if wdInt is None:
|
||||
return False
|
||||
elif self.__monitor.get_path(wdInt) is None or \
|
||||
self.__monitor.rm_watch(wdInt)[wdInt]:
|
||||
self.__watches[path] = None
|
||||
logSys.debug("Removed file watcher for %s", path)
|
||||
return True
|
||||
else:
|
||||
@@ -151,6 +157,7 @@ class FilterPyinotify(FileFilter):
|
||||
def _delLogPath(self, path):
|
||||
if not self._delFileWatcher(path):
|
||||
logSys.error("Failed to remove watch on path: %s", path)
|
||||
del self.__watches[path]
|
||||
|
||||
path_dir = dirname(path)
|
||||
if not len([k for k in self.__watches
|
||||
@@ -184,9 +191,10 @@ class FilterPyinotify(FileFilter):
|
||||
def stop(self):
|
||||
super(FilterPyinotify, self).stop()
|
||||
|
||||
# Stop the notifier thread
|
||||
self.__notifier.stop()
|
||||
self.__notifier.join() # to not exit before notifier does
|
||||
# Stop the notifier thread if it was ran and notifier was created
|
||||
if self.__notifier is not None:
|
||||
self.__notifier.stop()
|
||||
self.__notifier.join() # to not exit before notifier does
|
||||
self.__cleanup() # for pedantic ones
|
||||
|
||||
##
|
||||
|
||||
@@ -35,9 +35,10 @@ class ExecuteActions(unittest.TestCase):
|
||||
"""Call before every test case."""
|
||||
self.__jail = DummyJail()
|
||||
self.__actions = Actions(self.__jail)
|
||||
self.__tmpfile, self.__tmpfilename = tempfile.mkstemp()
|
||||
self.__tmpfile, self.__tmpfilename = tempfile.mkstemp('fail2ban', 'executeactions')
|
||||
|
||||
def tearDown(self):
|
||||
os.close(self.__tmpfile)
|
||||
os.remove(self.__tmpfilename)
|
||||
|
||||
def defaultActions(self):
|
||||
|
||||
@@ -17,65 +17,69 @@
|
||||
# failJSON: { "time": "2013-07-17T23:20:45", "match": true , "host": "127.0.0.1" }
|
||||
[Wed Jul 17 23:20:45 2013] [error] [client 127.0.0.1] client denied by server configuration: /var/www/html/noentry/cant_get_me.html
|
||||
|
||||
# failJSON: { "time": "2013-07-20T21:34:49", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:34:49", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:34:49.453232 2013] [access_compat:error] [pid 17512:tid 140123104306944] [client 127.0.0.1:51380] AH01797: client denied by server configuration: /var/www/html/noentry/cant_get_me.html
|
||||
|
||||
# wget --http-user='' --http-password='' http://localhost/basic/file/cant_get_me.html -O /dev/null
|
||||
# failJSON: { "time": "2013-07-17T23:14:37", "match": true , "host": "127.0.0.1" }
|
||||
[Wed Jul 17 23:14:37 2013] [error] [client 127.0.0.1] user not found: /basic/anon/cant_get_me.html
|
||||
|
||||
# failJSON: { "time": "2013-07-20T21:37:32", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:37:32", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:37:32.266605 2013] [auth_basic:error] [pid 17512:tid 140123079128832] [client 127.0.0.1:51386] AH01618: user not found: /basic/file/cant_get_me.html
|
||||
|
||||
# wget --http-user=username --http-password=wrongpass http://localhost/basic/file -O /dev/null
|
||||
# failJSON: { "time": "2013-07-17T22:18:52", "match": true , "host": "127.0.0.1" }
|
||||
[Wed Jul 17 22:18:52 2013] [error] [client 127.0.0.1] user username: authentication failure for "/basic/file": Password Mismatch
|
||||
|
||||
# failJSON: { "time": "2013-07-20T21:39:11", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:39:11", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:39:11.978080 2013] [auth_basic:error] [pid 17512:tid 140123053950720] [client 127.0.0.1:51390] AH01617: user username: authentication failure for "/basic/file": Password Mismatch
|
||||
|
||||
# wget --http-user=wrongusername --http-password=wrongpass http://localhost/basic/file -O /dev/null
|
||||
# failJSON: { "time": "2013-07-17T22:32:48", "match": true , "host": "127.0.0.1" }
|
||||
[Wed Jul 17 22:32:48 2013] [error] [client 127.0.0.1] user wrongusername not found: /basic/file
|
||||
|
||||
# failJSON: { "time": "2013-07-20T21:40:33", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:40:33", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:40:33.803528 2013] [auth_basic:error] [pid 17540:tid 140123095914240] [client 127.0.0.1:51395] AH01618: user wrongusername not found: /basic/file
|
||||
|
||||
# wget --header='Authorization: Digest username="Mufasa",realm="testrealm@host.com",nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",uri="/dir/index.html",qop=auth,nc=00000001,cnonce="0a4f113b",response="6629fae49393a05397450978507c4ef1",opaque="5ccc069c403ebaf9f0171e9517f40e41"' http://localhost/basic/file -O /dev/null
|
||||
# failJSON: { "time": "2013-07-17T22:39:55", "match": true , "host": "127.0.0.1" }
|
||||
[Wed Jul 17 22:39:55 2013] [error] [client 127.0.0.1] client used wrong authentication scheme: /basic/file
|
||||
|
||||
# failJSON: { "time": "2013-07-20T21:41:52", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:41:52", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:41:52.523931 2013] [auth_basic:error] [pid 17512:tid 140122964092672] [client 127.0.0.1:51396] AH01614: client used wrong authentication scheme: /basic/file
|
||||
|
||||
# wget --http-user=username --http-password=password http://localhost/basic/authz_owner/cant_get_me.html -O /dev/null
|
||||
# failJSON: { "time": "2013-07-17T22:54:32", "match": true , "host": "127.0.0.1" }
|
||||
[Wed Jul 17 22:54:32 2013] [error] [client 127.0.0.1] Authorization of user username to access /basic/authz_owner/cant_get_me.html failed, reason: file owner dan does not match.
|
||||
|
||||
# failJSON: { "time": "2013-07-20T22:11:43", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T22:11:43", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 22:11:43.147674 2013] [authz_owner:error] [pid 17540:tid 140122922129152] [client 127.0.0.1:51548] AH01637: Authorization of user username to access /basic/authz_owner/cant_get_me.html failed, reason: file owner dan does not match
|
||||
|
||||
# wget --http-user=username --http-password=password http://localhost/basic/authz_owner/cant_get_me.html -O /dev/null
|
||||
# failJSON: { "time": "2013-07-20T21:42:44", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:42:44", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:42:44.304159 2013] [authz_core:error] [pid 17484:tid 140123095914240] [client 127.0.0.1:51397] AH01631: user username: authorization failure for "/basic/authz_owner/cant_get_me.html":
|
||||
# failJSON: { "time": "2013-07-20T21:42:44", "match": true , "host": "127.0.0.1", "desc": "a version of previous case without subsec resolution" }
|
||||
[Sat Jul 20 21:42:44 2013] [authz_core:error] [pid 17484:tid 140123095914240] [client 127.0.0.1:51397] AH01631: user username: authorization failure for "/basic/authz_owner/cant_get_me.html":
|
||||
|
||||
# wget --http-user='username' --http-password='wrongpassword' http://localhost/digest/cant_get_me.html -O /dev/null
|
||||
# failJSON: { "time": "2013-07-17T23:50:37", "match": true , "host": "127.0.0.1" }
|
||||
[Wed Jul 17 23:50:37 2013] [error] [client 127.0.0.1] Digest: user username: password mismatch: /digest/cant_get_me.html
|
||||
|
||||
# failJSON: { "time": "2013-07-20T21:44:06", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:44:06", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:44:06.867985 2013] [auth_digest:error] [pid 17540:tid 140123070736128] [client 127.0.0.1:51406] AH01792: user username: password mismatch: /digest/cant_get_me.html
|
||||
|
||||
# wget --http-user='username' --http-password='password' http://localhost/digest_wrongrelm/cant_get_me.html -O /dev/null
|
||||
# failJSON: { "time": "2013-07-18T00:08:39", "match": true , "host": "127.0.0.1" }
|
||||
[Thu Jul 18 00:08:39 2013] [error] [client 127.0.0.1] Digest: user `username' in realm `digest private area' not found: /digest_wrongrelm/cant_get_me.html
|
||||
|
||||
# failJSON: { "time": "2013-07-20T21:45:28", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-20T21:45:28", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sat Jul 20 21:45:28.890523 2013] [auth_digest:error] [pid 17540:tid 140122972485376] [client 127.0.0.1:51408] AH01790: user `username' in realm `digest private area' not found: /digest_wrongrelm/cant_get_me.html
|
||||
|
||||
# ./testcases/files/config/apache-auth/digest.py
|
||||
# failJSON: { "time": "2013-07-28T21:05:31", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-28T21:05:31", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sun Jul 28 21:05:31.178340 2013] [auth_digest:error] [pid 24224:tid 139895539455744] [client 127.0.0.1:56906] AH01793: invalid qop `auth' received: /digest/qop_none/
|
||||
# failJSON: { "time": "2013-07-28T21:05:31", "match": true , "host": "127.0.0.1", "desc": "a version of previous case without subsec resolution" }
|
||||
[Sun Jul 28 21:05:31 2013] [auth_digest:error] [pid 24224:tid 139895539455744] [client 127.0.0.1:56906] AH01793: invalid qop `auth' received: /digest/qop_none/
|
||||
|
||||
# ./testcases/files/config/apache-auth/digest.py
|
||||
# failJSON: { "time": "2013-07-28T21:12:44", "match": true , "host": "127.0.0.1" }
|
||||
@@ -88,14 +92,14 @@
|
||||
[Sun Jul 28 21:16:37 2013] [error] [client 127.0.0.1] Digest: invalid nonce l19lgpDiBAZZZf1ec3d9613f3b3ef43660e3628d78455fd8b937 received - hash is not 6fda8bbcbcf85ff1ebfe7d1c43faba583bc53a02
|
||||
|
||||
# ./testcases/files/config/apache-auth/digest.py
|
||||
# failJSON: { "time": "2013-07-28T21:18:11", "match": false , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-28T21:18:11", "match": false , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sun Jul 28 21:18:11.769228 2013] [auth_digest:error] [pid 24752:tid 139895505884928] [client 127.0.0.1:56964] AH01776: invalid nonce b9YAiJDiBAZZZ1b1abe02d20063ea3b16b544ea1b0d981c1bafe received - hash is not d42d824dee7aaf50c3ba0a7c6290bd453e3dd35b
|
||||
|
||||
# ./testcases/files/config/apache-auth/digest.py
|
||||
# failJSON: { "time": "2013-07-28T21:30:02", "match": true , "host": "127.0.0.1" }
|
||||
[Sun Jul 28 21:30:02 2013] [error] [client 127.0.0.1] Digest: realm mismatch - got `so far away' but expected `digest private area'
|
||||
|
||||
# failJSON: { "time": "2013-07-28T21:27:56", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-28T21:27:56", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sun Jul 28 21:27:56.549667 2013] [auth_digest:error] [pid 24835:tid 139895297222400] [client 127.0.0.1:57052] AH01788: realm mismatch - got `so far away' but expected `digest private area'
|
||||
|
||||
|
||||
@@ -103,14 +107,14 @@
|
||||
# failJSON: { "time": "2013-07-28T21:41:20", "match": true , "host": "127.0.0.1" }
|
||||
[Sun Jul 28 21:41:20 2013] [error] [client 127.0.0.1] Digest: unknown algorithm `super funky chicken' received: /digest/
|
||||
|
||||
# failJSON: { "time": "2013-07-28T21:42:03", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-28T21:42:03", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Sun Jul 28 21:42:03.930190 2013] [auth_digest:error] [pid 24835:tid 139895505884928] [client 127.0.0.1:57115] AH01789: unknown algorithm `super funky chicken' received: /digest/
|
||||
|
||||
# ./testcases/files/config/apache-auth/digest.py
|
||||
# failJSON: { "time": "2013-07-29T02:15:26", "match": true , "host": "127.0.0.1" }
|
||||
[Mon Jul 29 02:15:26 2013] [error] [client 127.0.0.1] Digest: invalid nonce LWEDr5TiBAA=ceddd011628c30e3646f7acda4f1a0ab6b7c5ae6 received - user attempted time travel
|
||||
|
||||
# failJSON: { "time": "2013-07-29T02:12:55", "match": true , "host": "127.0.0.1" }
|
||||
# failJSON: { "time": "2013-07-29T02:12:55", "match": true , "host": "127.0.0.1", "minimal_python": "2.6" }
|
||||
[Mon Jul 29 02:12:55.539813 2013] [auth_digest:error] [pid 9647:tid 139895522670336] [client 127.0.0.1:58474] AH01777: invalid nonce 59QJppTiBAA=b08983fd166ade9840407df1b0f75b9e6e07d88d received - user attempted time travel
|
||||
|
||||
# failJSON: { "time": "2013-06-01T02:17:42", "match": true , "host": "192.168.0.2" }
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# failJSON: { "time": "2013-06-01T11:23:08", "match": true , "host": "1.2.3.4" }
|
||||
[Sat Jun 01 11:23:08 2013] [error] [client 1.2.3.4] File does not exist: /xxx/~
|
||||
# Apache 2.4
|
||||
# failJSON: { "time": "2013-06-27T11:55:44", "match": true , "host": "192.0.2.12" }
|
||||
# failJSON: { "time": "2013-06-27T11:55:44", "match": true , "host": "192.0.2.12", "minimal_python": "2.6" }
|
||||
[Thu Jun 27 11:55:44.569531 2013] [core:info] [pid 4101:tid 2992634688] [client 192.0.2.12:46652] AH00128: File does not exist: /xxx/~
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
# failJSON: { "time": "2008-07-22T06:48:30", "match": true , "host": "198.51.100.86" }
|
||||
[Tue Jul 22 06:48:30 2008] [error] [client 198.51.100.86] script not found or unable to stat: /home/e-smith/files/ibays/Primary/cgi-bin/php4
|
||||
# apache 2.4
|
||||
# failJSON: { "time": "2013-12-23T07:49:01", "match": true , "host": "204.232.202.107" }
|
||||
# failJSON: { "time": "2013-12-23T07:49:01", "match": true , "host": "204.232.202.107", "minimal_python": "2.6" }
|
||||
[Mon Dec 23 07:49:01.981912 2013] [:error] [pid 3790] [client 204.232.202.107:46301] script '/var/www/timthumb.php' not found or unable to stat
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2
|
||||
Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.119 port 51332 ssh2
|
||||
Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2
|
||||
|
||||
+14
-10
@@ -280,11 +280,15 @@ class LogFileFilterPoll(unittest.TestCase):
|
||||
class LogFileMonitor(LogCaptureTestCase):
|
||||
"""Few more tests for FilterPoll API
|
||||
"""
|
||||
|
||||
_setup_idx = 0 # to ease tracking of dangling opened files
|
||||
|
||||
def setUp(self):
|
||||
"""Call before every test case."""
|
||||
LogCaptureTestCase.setUp(self)
|
||||
self.filter = self.name = 'NA'
|
||||
_, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures')
|
||||
_, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures-%d-' % LogFileMonitor._setup_idx)
|
||||
LogFileMonitor._setup_idx += 1
|
||||
self.file = open(self.name, 'a')
|
||||
self.filter = FilterPoll(None)
|
||||
self.filter.addLogPath(self.name)
|
||||
@@ -550,12 +554,12 @@ def get_monitor_failures_testcase(Filter_):
|
||||
self.assertEqual(self.filter.failManager.getFailTotal(), 3)
|
||||
|
||||
if interim_kill:
|
||||
_killfile(None, self.name)
|
||||
_killfile(self.file, self.name)
|
||||
time.sleep(0.2) # let them know
|
||||
|
||||
# now create a new one to override old one
|
||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.name + '.new',
|
||||
n=100).close()
|
||||
n=100, skip=3).close()
|
||||
os.rename(self.name + '.new', self.name)
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01)
|
||||
self.assertEqual(self.filter.failManager.getFailTotal(), 6)
|
||||
@@ -713,12 +717,12 @@ class GetFailures(unittest.TestCase):
|
||||
|
||||
def testGetFailuresUseDNS(self):
|
||||
# We should still catch failures with usedns = no ;-)
|
||||
output_yes = ('93.184.216.119', 2, 1124013539.0,
|
||||
output_yes = ('93.184.216.34', 2, 1124013539.0,
|
||||
['Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2\n',
|
||||
'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.119 port 51332 ssh2\n'])
|
||||
'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2\n'])
|
||||
|
||||
output_no = ('93.184.216.119', 1, 1124013539.0,
|
||||
['Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.119 port 51332 ssh2\n'])
|
||||
output_no = ('93.184.216.34', 1, 1124013539.0,
|
||||
['Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2\n'])
|
||||
|
||||
# Actually no exception would be raised -- it will be just set to 'no'
|
||||
#self.assertRaises(ValueError,
|
||||
@@ -765,9 +769,9 @@ class DNSUtilsTests(unittest.TestCase):
|
||||
res = DNSUtils.textToIp('www.example.com', 'no')
|
||||
self.assertEqual(res, [])
|
||||
res = DNSUtils.textToIp('www.example.com', 'warn')
|
||||
self.assertEqual(res, ['93.184.216.119'])
|
||||
self.assertEqual(res, ['93.184.216.34'])
|
||||
res = DNSUtils.textToIp('www.example.com', 'yes')
|
||||
self.assertEqual(res, ['93.184.216.119'])
|
||||
self.assertEqual(res, ['93.184.216.34'])
|
||||
|
||||
def testTextToIp(self):
|
||||
# Test hostnames
|
||||
@@ -779,7 +783,7 @@ class DNSUtilsTests(unittest.TestCase):
|
||||
for s in hostnames:
|
||||
res = DNSUtils.textToIp(s, 'yes')
|
||||
if s == 'www.example.com':
|
||||
self.assertEqual(res, ['93.184.216.119'])
|
||||
self.assertEqual(res, ['93.184.216.34'])
|
||||
else:
|
||||
self.assertEqual(res, [])
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ __license__ = "GPL"
|
||||
import unittest, sys, os, fileinput, re, datetime, inspect
|
||||
from ConfigParser import InterpolationMissingOptionError
|
||||
|
||||
if sys.version_info >= (2, 6):
|
||||
from distutils.version import LooseVersion
|
||||
python_version = LooseVersion(sys.version)
|
||||
|
||||
if python_version >= "2.6":
|
||||
import json
|
||||
else:
|
||||
import simplejson as json
|
||||
@@ -108,6 +111,10 @@ def testSampleRegexsFactory(name):
|
||||
self.assertEqual(len(ret), 1, "Multiple regexs matched %r - %s:%i" %
|
||||
(map(lambda x: x[0], ret),logFile.filename(), logFile.filelineno()))
|
||||
|
||||
# Some dates matching might not be supported on by a given Python
|
||||
if python_version < str(faildata.get("minimal_python", "2")):
|
||||
print "Skipped testing the date for line #%s" % (line,)
|
||||
continue # skipping this test
|
||||
# Verify timestamp and host as expected
|
||||
failregex, host, time = ret[0]
|
||||
self.assertEqual(host, faildata.get("host", None))
|
||||
|
||||
@@ -160,7 +160,7 @@ class Transmitter(TransmitterBase):
|
||||
self.assertEqual(self.transm.proceed(["sleep", "1"]), (0, None))
|
||||
t1 = time.time()
|
||||
# Approx 1 second delay
|
||||
self.assertAlmostEqual(t1 - t0, 1, places=2)
|
||||
self.assertAlmostEqual(t1 - t0, 1, places=1)
|
||||
|
||||
def testAddJail(self):
|
||||
jail2 = "TestJail2"
|
||||
@@ -513,8 +513,13 @@ class TransmitterLogging(TransmitterBase):
|
||||
|
||||
self.setGetTest("logtarget", "STDOUT")
|
||||
self.setGetTest("logtarget", "STDERR")
|
||||
if sys.platform.lower().startswith('linux'):
|
||||
self.setGetTest("logtarget", "SYSLOG")
|
||||
|
||||
def testLogTargetSYSLOG(self):
|
||||
if not os.path.exists("/dev/log") and sys.version_info >= (2, 7):
|
||||
raise unittest.SkipTest("'/dev/log' not present")
|
||||
elif not os.path.exists("/dev/log"):
|
||||
return
|
||||
self.setGetTest("logtarget", "SYSLOG")
|
||||
|
||||
def testLogLevel(self):
|
||||
self.setGetTest("loglevel", "4", 4)
|
||||
|
||||
Reference in New Issue
Block a user