diff --git a/languages/ganneffserv.en.lang b/languages/ganneffserv.en.lang index 3f18c4b..6b4c26a 100644 --- a/languages/ganneffserv.en.lang +++ b/languages/ganneffserv.en.lang @@ -75,3 +75,18 @@ GS_HLP_SRV_LONG IN ROTATION. EVERY user that connects on this server WILL BE KLINED! +GS_HLP_PRT_SHORT + %s: Protect users from akill triggered by another similar user +GS_HLP_PRT_LONG + Takes a hostmask (or a regular expression if it starts with ^). + + When a matching user trips on a trap, only they will be killed + instead of causing collateral damage to other matching users. + + Usage: PROTECT : +GS_HLP_UPR_SHORT + %s: Delete protection for adjacent users +GS_HLP_UPR_LONG + Delete collateral damage protection for matching users. + + Usage: UNPROTECT  diff --git a/modules/GanneffServ.rb b/modules/GanneffServ.rb index f9abcd6..dfc2bb7 100644 --- a/modules/GanneffServ.rb +++ b/modules/GanneffServ.rb @@ -69,6 +69,8 @@ def initialize() ["STATS", 0, 0, SFLG_NOMAXPARAM, ADMIN_FLAG, lm('GS_HLP_STS_SHORT'), lm('GS_HLP_STS_LONG')], ["ENFORCE", 0, 0, SFLG_NOMAXPARAM, ADMIN_FLAG, lm('GS_HLP_ENF_SHORT'), lm('GS_HLP_ENF_LONG')], ["BADSERV", 0, 1, SFLG_NOMAXPARAM, ADMIN_FLAG, lm('GS_HLP_SRV_SHORT'), lm('GS_HLP_SRV_LONG')], + ["PROTECT", 1, 2, SFLG_NOMAXPARAM, ADMIN_FLAG, lm('GS_HLP_PRT_SHORT'), lm('GS_HLP_PRT_LONG')], + ["UNPROTECT",1, 2, SFLG_NOMAXPARAM, ADMIN_FLAG, lm('GS_HLP_UPR_SHORT'), lm('GS_HLP_UPR_LONG')], ]) # register # Which hooks do we want? @@ -100,6 +102,13 @@ def initialize() irc_lower($1)') @dbq['INCREASE_KILLS'] = DB.prepare('UPDATE ganneffserv SET kills = kills+1 WHERE irc_lower(channel) = irc_lower($1)') + @dbq['INSERT_PROTECT'] = DB.prepare('INSERT INTO ganneffprotect(setter, time, + pattern, reason) VALUES($1, $2, $3, $4)') + @dbq['DELETE_PROTECT'] = DB.prepare('DELETE FROM ganneffprotect WHERE + irc_lower(pattern) = irc_lower($1)') + @dbq['GET_PROTECTED_PATTERNS'] = DB.prepare('SELECT pattern, reason FROM ganneffprotect') + @dbq['GET_PROTECTED_PATTERNS_DETAILED'] = DB.prepare('SELECT pattern, setter, time, + reason FROM ganneffprotect') end # def initialize ######################################################################## @@ -221,6 +230,50 @@ def DEL(client, parv = []) true end # def DEL +# ------------------------------------------------------------------------ + + # Protect users from collatoral damage + def PROTECT(client, parv = []) + parv[1].downcase! + debug(LOG_DEBUG, "#{client.name} called PROTECT and the params are #{parv.join(",")}") + + requested_pattern = parv[1] + pattern = irc_pattern_to_regex(requested_pattern) + reason = parv[2] + + ret = DB.execute_nonquery(@dbq['INSERT_PROTECT'], 'iiss', client.nick.account_id, + Time.now.to_i, pattern, reason) + if ret then + debug(LOG_NOTICE, "#{client.name} added protection #{pattern}, reason #{reason}") + @protection[pattern] = reason + load_protected_patterns + reply(client, "Protection #{requested_pattern} successfully added") + else + reply(client, "Failed to add #{requested_pattern}") + end + end + +# ------------------------------------------------------------------------ + + # Unprotect users from collatoral damage + def UNPROTECT(client, parv = []) + parv[1].downcase! + debug(LOG_DEBUG, "#{client.name} called UNPROTECT and the params are #{parv.join(",")}") + + pattern = irc_pattern_to_regex(parv[1]) + return unless @protection.has_key?(pattern) + + ret = DB.execute_nonquery(@dbq['DELETE_PROTECT'], 's', pattern) + if ret then + debug(LOG_NOTICE, "#{client.name} removed protection #{pattern}") + @protection.delete(pattern) + load_protected_patterns + reply(client, "Protection #{pattern} successfully deleted.") + else + reply(client, "Failed to delete protection #{pattern}.") + end + end + # ------------------------------------------------------------------------ # List all channels we monitor @@ -245,6 +298,18 @@ def LIST(client, parv = []) } result.free + reply(client, "Protected host patterns\n\n") + reply(client, "%-50s %-10s %-19s %s" % [ "Pattern", "By", "When", "Reason" ]) + result = DB.execute(@dbq['GET_PROTECTED_PATTERNS_DETAILED']) + result.row_each { |row| + pattern = row[0] + by = row[1] + time = Time.at(row[2].to_i).strftime('%Y-%m-%d %H:%M:%S') + reason = row[3] + reply(client, "%-50s %-10s %-19s %s" % [ pattern, by, time, reason ]) + } + result.free + reply(client, "\nCRFJ - checks Connect, Register nick, Join channel within 15 seconds (i.e. Fast)") reply(client, "J - triggers on every Join") @@ -326,7 +391,7 @@ def BADSERV(client, parv = []) debug(LOG_DEBUG, "#{client.name} called BADSERV and the parms are #{parv.join(",")}") server = parv[1].downcase - if server =~ /.*\.oftc.net$/ + if server =~ /.*\.oftc\.net$/ debug(LOG_DEBUG, "#{server} seems to be an oftc server, proceeding") @badserver = server reply(client, "#{server} is now marked as a bad server, all new connections will be killed") @@ -574,8 +639,13 @@ def akill(client, reason, operreason, channel="") ret = kill_user(client, reason) else # if host reason = "#{reason}|#{operreason}" - debug(LOG_DEBUG, "Issuing AKILL: *@#{host}, #{reason} lasting for #{@akill_duration} seconds") - ret = akill_add("*@#{host}", reason, @akill_duration) + if client.host =~ /#{@protected_patterns}/i # if protected hosts + debug(LOG_DEBUG, "Using /kill instead of AKILL for protected user #{client.name}") + ret = kill_user(client, reason) + else + debug(LOG_DEBUG, "Issuing AKILL: *@#{host}, #{reason} lasting for #{@akill_duration} seconds") + ret = akill_add("*@#{host}", reason, @akill_duration) + end # if protected hosts end # if host channel.downcase! @@ -596,6 +666,31 @@ def akill(client, reason, operreason, channel="") end # if kill_user end # def akill +# ------------------------------------------------------------------------ + + # convert irc pattern to regular expression + def irc_pattern_to_regex(pattern + # "." -> "\.", "*" -> ".*", "?" -> "." + # wrap with "^...$" + return pattern if pattern.start_with? '^' + pattern = pattern.gsub(/\./, '\\.') + .gsub(/\*/, '.*') + .gsub(/\?/, '.') + return "^#{pattern}$" + end + +# ------------------------------------------------------------------------ + + # get protected patterns as pattern + def load_protected_patterns() + patterns = @protection.keys + if patterns.empty? + @protected_patterns = '^$' + else + @protected_patterns = patterns.join('|') + end + end # def get_protected_patterns + # ------------------------------------------------------------------------ # enforce a channel - kill all of its users @@ -648,6 +743,20 @@ def load_data() count += 1 } result.free + + @protection = Hash.new + result = DB.execute(@dbq['GET_PROTECTED_PATTERNS'], '') + count = 0 + result.row_each { |row| + pattern = row[0] + reason = row[1] + pattern = irc_pattern_to_regex(pattern) + @protection[pattern] = reason + count += 1 + } + result.free + load_protected_patterns + debug(LOG_DEBUG, "All channel data successfully loaded") end # def load_data diff --git a/sql/ganneffserv-pgsql.sql b/sql/ganneffserv-pgsql.sql index 9f8feb8..e24fe19 100644 --- a/sql/ganneffserv-pgsql.sql +++ b/sql/ganneffserv-pgsql.sql @@ -9,3 +9,13 @@ CREATE TABLE ganneffserv ( monitor_only BOOLEAN NOT NULL DEFAULT 'False' ); CREATE UNIQUE INDEX ganneffserv_channel_idx ON ganneffserv (irc_lower(channel)); + +DROP TABLE IF EXISTS ganneffprotect CASCADE; +CREATE TABLE ganneffprotect ( + id SERIAL PRIMARY KEY, + setter INTEGER REFERENCES account(id) ON DELETE SET NULL, + time INTEGER NOT NULL, + pattern VARCHAR(255) NOT NULL, + reason VARCHAR(255) NOT NULL +); +CREATE UNIQUE INDEX ganneffservprotect_pattern_idx ON ganneffservprotect (irc_lower(pattern));