| # vim: set tabstop=4 shiftwidth=4 expandtab syntax=perl: |
| # vim: set tabstop=4 shiftwidth=4 expandtab syntax=perl: |
| # |
| # |
| # Copyright 1999 Jeremy Nixon <jeremy@exit109.com> |
| # Copyright 1999 Jeremy Nixon <jeremy@exit109.com> |
| # Copyright 2001 Marco d'Itri <md@linux.it> |
| # Copyright 2001 Marco d'Itri <md@linux.it> |
| # |
| # |
| # Modified by Steve Crook <steve@mixmin.net> |
| # Modified by Steve Crook <steve@mixmin.net> |
| # Redistributed in accordance with the terms of the Artistic license. |
| # Redistributed in accordance with the terms of the Artistic license. |
| # |
| # |
| # This software is distributed under the terms of the Artistic License. |
| # This software is distributed under the terms of the Artistic License. |
| # Please see the LICENSE file in the distribution. |
| # Please see the LICENSE file in the distribution. |
| # |
| # |
| |
| |
| # CHANGE THE BELOW SETTING! |
| # CHANGE THE BELOW SETTING! |
| # Directory where cleanfeed.local and the other configuration files live. |
| # Directory where cleanfeed.local and the other configuration files live. |
| # Set this to undef to not use any external file. |
| # Set this to undef to not use any external file. |
| |
| |
| $config_dir = '/usr/inn/cleanfeed/etc'; |
| $config_dir = exists $ENV{CLEANFEED_CONFIG_DIR} |
| |
| ? ($ENV{CLEANFEED_CONFIG_DIR} eq '' ? undef : $ENV{CLEANFEED_CONFIG_DIR})
|
| |
| : '/usr/local/news/cleanfeed/etc';
|
| |
| |
| ############################################################################## |
| ############################################################################## |
| # Server configuration |
| # Server configuration |
| # |
| # |
| # Set $MODE according to what you're running. |
| # Set $MODE according to what you're running. |
| # Acceptable values: inn, highwind. |
| # Acceptable values: inn, highwind. |
| # If you are running a highwind-like server then the value set here is ignored |
| # If you are running a highwind-like server then the value set here is ignored |
| # and the default from highwind.pl is used. |
| # and the default from highwind.pl is used. |
| |
| |
| $MODE ||= 'inn'; |
| $MODE ||= 'inn'; |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # WARNING: NO USER SERVICEABLE PARTS BELOW THIS LINE |
| # WARNING: NO USER SERVICEABLE PARTS BELOW THIS LINE |
| # IF YOU WANT TO CHANGE SOMETHING, PLEASE USE cleanfeed.local |
| # IF YOU WANT TO CHANGE SOMETHING, PLEASE USE cleanfeed.local |
| ############################################################################## |
| ############################################################################## |
| |
| |
| |
| # Cleanfeed 2026 maintained release: 2026.08
|
| |
| $cleanfeed_maintenance_version = '2026.08';
|
| |
|
|
| # default configuration |
| # default configuration |
| sub get_config { |
| sub get_config { |
| %config = ( |
| %config = ( |
| verbose => 1, # verbose rejection reasons in news.notice/logfile? |
| verbose => 1, # verbose rejection reasons in news.notice/logfile? |
| aggressive => 1, # set to 0 if your lawyers are paranoid |
| aggressive => 1, # set to 0 if your lawyers are paranoid |
| maxgroups => 14, # maximum number of groups in a crosspost |
| maxgroups => 14, # maximum number of groups in a crosspost |
| block_binaries => 1, # block misplaced binaries |
| block_binaries => 1, # block misplaced binaries |
| block_all_binaries => 0, # Reject all binary regardless of distribution |
| block_all_binaries => 0, # Reject all binary regardless of distribution |
| block_late_cancels => 0, # block cancels of rejected articles |
| block_late_cancels => 0, # block cancels of rejected articles |
| block_user_spamcancels => 1,# reject spam cancels |
| block_user_spamcancels => 1,# reject spam cancels |
| block_user_cancels => 0, # accept only spam cancels |
| block_user_cancels => 0, # accept only spam cancels |
| block_extra_reposts => 1, # block reposts for articles not cancelled |
| block_extra_reposts => 1, # block reposts for articles not cancelled |
| |
| |
| do_md5 => 1, # do the md5 checks? |
| do_md5 => 1, # do the md5 checks? |
| do_phl => 1, # do the posting-host/lines EMP check? |
| do_phl => 1, # do the posting-host/lines EMP check? |
| do_phn => 1, # do the posting-host/newsgroups EMP check? |
| do_phn => 1, # do the posting-host/newsgroups EMP check? |
| do_phr => 1, # do posting-host (high risk groups) check? |
| do_phr => 1, # do posting-host (high risk groups) check? |
| do_fsl => 1, # do the from/subject/lines EMP check? |
| do_fsl => 1, # do the from/subject/lines EMP check? |
| do_scoring_filter => 1, # use the scoring filter? |
| do_scoring_filter => 1, # use the scoring filter? |
| do_ratio_scoring => 0, # Score articles based on Caps/Sym/URL ratios? |
| do_ratio_scoring => 0, # Score articles based on Caps/Sym/URL ratios? |
| bad_url_score => 5, # What score to apply to bad_url hits |
| bad_url_score => 5, # What score to apply to bad_url hits |
| |
| |
| do_emp_dump => 1, # dump EMP histories to a file for persistence? |
| do_emp_dump => 1, # dump EMP histories to a file for persistence? |
| emp_dump_file => '', # file to dump EMP histories to |
| emp_dump_file => '', # file to dump EMP histories to |
| |
| |
| MD5RateCutoff => 5, # reject if this many copies are in the history |
| MD5RateCutoff => 5, # reject if this many copies are in the history |
| MD5RateCeiling => 85, # only count this high |
| MD5RateCeiling => 85, # only count this high |
| MD5RateBaseInterval => 7200,# How long to wait before decrementing the count |
| MD5RateBaseInterval => 7200,# How long to wait before decrementing the count |
| PHLRateCutoff => 20, |
| PHLRateCutoff => 20, |
| PHLRateCeiling => 80, |
| PHLRateCeiling => 80, |
| PHLRateBaseInterval => 3600, |
| PHLRateBaseInterval => 3600, |
| PHNRateCutoff => 150, |
| PHNRateCutoff => 150, |
| PHNRateCeiling => 200, |
| PHNRateCeiling => 200, |
| PHNRateBaseInterval => 1800, |
| PHNRateBaseInterval => 1800, |
| PHRRateCutoff => 10, |
| PHRRateCutoff => 10, |
| PHRRateCeiling => 80, |
| PHRRateCeiling => 80, |
| PHRRateBaseInterval => 3600, |
| PHRRateBaseInterval => 3600, |
| FSLRateCutoff => 20, |
| FSLRateCutoff => 20, |
| FSLRateCeiling => 40, |
| FSLRateCeiling => 40, |
| FSLRateBaseInterval => 1800, |
| FSLRateBaseInterval => 1800, |
| |
| |
| fuzzy_md5 => 1, # screw around with the body before md5ing? |
| fuzzy_md5 => 1, # screw around with the body before md5ing? |
| fuzzy_max_length => 700, # don't screw with bodies over this many lines |
| fuzzy_max_length => 700, # don't screw with bodies over this many lines |
| md5_max_length => 2000, # don't md5 articles over this many lines |
| md5_max_length => 2000, # don't md5 articles over this many lines |
| trim_interval => 900, # trim hashes every N seconds |
| trim_interval => 900, # trim hashes every N seconds |
| stats_interval => 3600, # write status file every N seconds |
| stats_interval => 3600, # write status file every N seconds |
| MIDmaxlife => 4, # time to keep rejected message-ids, in hours |
| MIDmaxlife => 4, # time to keep rejected message-ids, in hours |
| md5_skips_followups => 1, # avoid MD5 check on articles with References? |
| md5_skips_followups => 1, # avoid MD5 check on articles with References? |
| phn_aggressive => 1, # use path for phn filter when no posting host |
| phn_aggressive => 1, # use path for phn filter when no posting host |
| phr_aggressive => 1, # use path for phr filter when no posting host |
| phr_aggressive => 1, # use path for phr filter when no posting host |
| do_mid_filter => 1, # use the message-id CHECK filter? (INN only) |
| do_mid_filter => 1, # use the message-id CHECK filter? (INN only) |
| do_supersedes_filter => 1, # do the excessive supersedes filter? |
| do_supersedes_filter => 1, # do the excessive supersedes filter? |
| |
| supersedes_mode => 'reject', # off, audit, reject
|
| |
| supersedes_window => 900, # rate window/base interval in seconds
|
| |
| supersedes_ceiling => 50, # maximum retained count per source
|
| |
| supersedes_max_faq => 45,
|
| |
| supersedes_max_unmoderated => 6,
|
| |
| supersedes_max_moderated => 10,
|
| |
| supersedes_max_all_moderated => 35,
|
| |
| supersedes_max_unknown_active => 10,
|
| drop_useless_controls => 1, # drop sendsys, senduuname, version control msg |
| drop_useless_controls => 1, # drop sendsys, senduuname, version control msg |
| drop_ihave_sendme => 1, # drop ihave, sendme control messages |
| drop_ihave_sendme => 1, # drop ihave, sendme control messages |
| bad_rate_reload => 10000, # Reload bad_* files after this many articles |
| bad_rate_reload => 10000, # Reload bad_* files after this many articles |
| |
| |
| |
| # Lightweight 2026 operational hardening.
|
| |
| bad_reload_mode => 'mtime', # mtime, articles, or both
|
| |
| bad_reload_interval => 60, # seconds between mtime checks
|
| |
| external_regex_max_length => 1048576,
|
| |
| external_regex_max_count => 5000,
|
| |
| external_regex_body_bytes => 65536,
|
| |
| external_regex_reject_unsafe => 1,
|
| |
|
|
| |
| # Article structure and lightweight malformed-encoding checks.
|
| |
| max_header_line_bytes => 16384, # 0 disables
|
| |
| max_body_line_bytes => 1048576, # 0 disables
|
| |
| long_line_mode => 'audit', # off, audit, reject
|
| |
| malformed_encoding_check => 1,
|
| |
| malformed_encoding_mode => 'audit', # off, audit, reject
|
| |
| yenc_size_tolerance_percent => 15,
|
| |
|
|
| |
| # Lightweight byte-profile detector for opaque binary payloads.
|
| |
| binary_byte_profile_enabled => 1,
|
| |
| binary_nonprintable_ratio_percent => 10,
|
| |
| binary_ratio_min_bytes => 4096,
|
| |
| binary_ratio_scan_bytes => 65536,
|
| |
| binary_byte_profile_mode => 'audit', # off, audit, reject
|
| |
|
|
| |
| # Path sanity and traffic anomaly audit.
|
| |
| path_sanity_enabled => 1,
|
| |
| path_max_hops => 100,
|
| |
| path_max_token_bytes => 255,
|
| |
| path_repeat_ceiling => 3,
|
| |
| path_sanity_mode => 'audit', # off, audit, reject
|
| |
| anomaly_rate_enabled => 1,
|
| |
| anomaly_window => 60,
|
| |
| anomaly_peer_articles => 10000,
|
| |
| anomaly_host_articles => 1000,
|
| |
| anomaly_mode => 'audit', # off, audit, reject
|
| |
| anomaly_max_keys => 1000,
|
| |
|
|
| |
| # Bounded top-offender counters and configuration fingerprint.
|
| |
| top_offenders_enabled => 1,
|
| |
| top_offenders_limit => 10,
|
| |
| top_offenders_max_keys => 1000,
|
| |
| config_fingerprint_enabled => 1,
|
| |
|
|
| |
| # Optional IDN normalization. No dependency is required; it activates only
|
| |
| # when Net::IDN::Encode is installed.
|
| |
| idn_normalization => 1,
|
| |
|
|
| |
| # Trusted-list bypass is deliberately granular; full bypass is discouraged.
|
| |
| trusted_bypass_scoring => 1,
|
| |
| trusted_bypass_content => 0,
|
| |
| trusted_bypass_binary => 0,
|
| |
| trusted_bypass_crosspost => 0,
|
| |
| trusted_bypass_size => 0,
|
| |
|
|
| low_xpost_maxgroups => 6, # max xposts in low_xpost_groups |
| low_xpost_maxgroups => 6, # max xposts in low_xpost_groups |
| meow_ext_maxgroups => 2, # max xposts from meow_groups to other groups |
| meow_ext_maxgroups => 2, # max xposts from meow_groups to other groups |
| off_topic1_maxgroups => 2, # How many off topic groups allowed in a distro |
| off_topic1_maxgroups => 2, # How many off topic groups allowed in a distro |
| on_topic1_maxgroups => 5, # How many on-topic groups allowed in a distro |
| on_topic1_maxgroups => 5, # How many on-topic groups allowed in a distro |
| on_topic1_mingroups => 2, # How many on-topic groups req'd to trigger |
| on_topic1_mingroups => 2, # How many on-topic groups req'd to trigger |
| # off-topic filter |
| # off-topic filter |
| off_topic2_maxgroups => 2, |
| off_topic2_maxgroups => 2, |
| on_topic2_maxgroups => 5, |
| on_topic2_maxgroups => 5, |
| on_topic2_mingroups => 2, |
| on_topic2_mingroups => 2, |
| |
| |
| binaries_in_mod_groups => 0, # allow binaries in moderated groups? |
| binaries_in_mod_groups => 0, # allow binaries in moderated groups? |
| max_base64_lines => 150, # Allow x lines Base64 encoding in non-bin grps |
| max_base64_lines => 150, # Allow x bare Base64 lines in non-bin grps |
| |
| binary_scan_bytes => 262144,# Scan this many bytes from article start
|
| |
| binary_scan_tail_bytes => 16384, # Also scan tail (yEnc terminators)
|
| |
| detect_mime_binaries => 1, # Detect MIME binary attachments, even if short
|
| |
| detect_malformed_yenc => 1, # Catch common non-compliant yEnc variants
|
| |
| |
| block_mime_html => 1, # block MIME encapsulated HTML |
| block_mime_html => 1, # block MIME encapsulated HTML |
| block_html_multipart => 1, # block all multipart with html sections |
| block_html_multipart => 1, # block all multipart with html sections |
| block_html => 1, # block native HTML (Content-Type text/html) |
| block_html => 1, # block native HTML (Content-Type text/html) |
| block_html_images => 1, # block <img src> in non text/plain messages |
| block_html_images => 1, # block <img src> in non text/plain messages |
| |
| |
| active_file => '', # active file to determine which groups are moderated |
| active_file => '', # active file to determine which groups are moderated |
| |
| |
| # Logging and pid_file don't work for INN (uses news.notice) |
| # Logging and pid_file don't work for INN (uses news.notice) |
| log_directory => '', |
| log_directory => '', |
| log_name => '', |
| log_name => '', |
| log_accepts => 0, # include accepted articles in the log? |
| log_accepts => 0, # include accepted articles in the log? |
| max_log_size => 0, |
| max_log_size => 0, |
| rotate_file => '', # rotate log if this file exists |
| rotate_file => '', # rotate log if this file exists |
| keep_old_logs => 7, # how many old logfiles to keep |
| keep_old_logs => 7, # how many old logfiles to keep |
| |
| |
| pid_file => '', |
| pid_file => '', |
| |
| |
| # crude stats on what the filter is doing |
| # crude stats on what the filter is doing |
| statfile => '', |
| statfile => '', |
| html_statfile => '', |
| html_statfile => '', |
| inn_syslog_status => 0, # status to syslog (late-model INN only) |
| inn_syslog_status => 0, # status to syslog (late-model INN only) |
| |
| |
| timer_info => 1, # timing information (arts/second) in status report? |
| timer_info => 1, # timing information (arts/second) in status report? |
| |
| body_preview_bytes => 16384, # decoded/lowercase preview used by legacy rules
|
| |
| study_max_lines => 250, # Perl study() only below this line count; 0 disables
|
| |
| validate_config => 1, # validate numeric values and regular expressions
|
| |
|
|
| |
| # Peer and hierarchy policy engine. The default is audit-only for safety.
|
| |
| policy_enabled => 1,
|
| |
| policy_mode => 'audit', # off, audit, quarantine, reject
|
| |
| policy_default_max_bytes => 0, # 0 means unlimited
|
| |
| policy_default_allow_binary => 1,
|
| |
| policy_log_matches => 1,
|
| |
| policy_log_accepts => 0,
|
| |
| policy_include_message_id => 1,
|
| |
| policy_include_peer => 1,
|
| |
| policy_include_groups => 1,
|
| |
| policy_max_peer_counters => 500,
|
| |
| policy_max_hierarchy_counters => 500,
|
| |
|
|
| |
| # Simple operational metrics. All outputs are optional.
|
| |
| metrics_enabled => 1,
|
| |
| metrics_status_file => '', # atomic key=value snapshot
|
| |
| metrics_csv_file => '', # append-only CSV history
|
| |
| metrics_csv_interval => 300,
|
| |
| metrics_syslog => 0,
|
| |
| metrics_by_rule => 1,
|
| |
| metrics_by_peer => 1,
|
| |
| metrics_by_hierarchy => 1,
|
| |
| metrics_prometheus_file => '', # optional node_exporter textfile
|
| |
| |
| debug_batch_directory => '', # directory for debugging batches |
| debug_batch_directory => '', # directory for debugging batches |
| debug_batch_size => 0, # max size of batch files before rotation |
| debug_batch_size => 0, # max size of batch files before rotation |
| |
| |
| ### binaries allowed if groups match |
| ### binaries allowed if groups match |
| bin_allowed => '^bin[a.]|\.bin[aei.]|\.bin$|^fur\.artwork'. |
| bin_allowed => '^bin[a.]|\.bin[aei.]|\.bin$|^fur\.artwork'. |
| '|^alt\.anonymous\.messages$|^de\.alt\.dateien|^rec\.games\.bolo$'. |
| '|^alt\.anonymous\.messages$|^de\.alt\.dateien|^rec\.games\.bolo$'. |
| '|^comp\.security\.pgp\.test$|^sfnet\.tiedostot'. |
| '|^comp\.security\.pgp\.test$|^sfnet\.tiedostot'. |
| '|^fido\.|^unidata\.|alt\.security\.keydist'. |
| '|^fido\.|^unidata\.|alt\.security\.keydist'. |
| '|^linux\.debian\.bugs\.dist$|^lucky\.freebsd', |
| '|^linux\.debian\.bugs\.dist$|^lucky\.freebsd', |
| |
| |
| # Groups matching this regex will accept binary UUenc and yEnc files |
| # Groups matching this regex will accept binary UUenc and yEnc files |
| # where filename extensions match 'image_extensions'. |
| # where filename extensions match 'image_extensions'. |
| image_allowed => '\.pictures', |
| image_allowed => '\.pictures', |
| |
| |
| # Extensions on image files that are allowed in 'image_allowed' groups. |
| # Extensions on image files that are allowed in 'image_allowed' groups. |
| # These are not case sensitive. |
| # These are not case sensitive. |
| image_extensions => 'jpe?g|png|gif|icon?', |
| image_extensions => 'jpe?g|png|gif|icon?', |
| |
| |
| ### no binaries allowed even if bin_allowed matches |
| ### no binaries allowed even if bin_allowed matches |
| bad_bin => '\.d$|^alt\.chello', |
| bad_bin => '\.d$|^alt\.chello', |
| |
| |
| ### md5 EMP check not done if groups match |
| ### md5 EMP check not done if groups match |
| md5exclude => '^perl\.cpan\.testers', |
| md5exclude => '^perl\.cpan\.testers', |
| |
| |
| ### reject all articles crossposted to groups matching this |
| ### reject all articles crossposted to groups matching this |
| poison_groups => '^alt\.(?:binaires|bainaries)|sexzilla|^newsmon$'. |
| poison_groups => '^alt\.(?:binaires|bainaries)|sexzilla|^newsmon$'. |
| '|h[i\d]pcl[o\d]ne|h\.i\.p\.c\.r\.i\.m\.e'. |
| '|h[i\d]pcl[o\d]ne|h\.i\.p\.c\.r\.i\.m\.e'. |
| ($] >= 5.005 ? '|(?<!free\.)h[i\d]pcr[i\d]m[e\d]' |
| ($] >= 5.005 ? '|(?<!free\.)h[i\d]pcr[i\d]m[e\d]' |
| : '|^alt\.hipcrime|^us\.hipcrime|^hipcrime|h\dpcr\dme'), |
| : '|^alt\.hipcrime|^us\.hipcrime|^hipcrime|h\dpcr\dme'), |
| |
| |
| ### no checks done if groups match |
| ### no checks done if groups match |
| allexclude => '^mailing\.|^linux\.', |
| allexclude => '^mailing\.|^linux\.', |
| |
| |
| ### Exclude matching Newsgroups from the scoring filters |
| ### Exclude matching Newsgroups from the scoring filters |
| score_exclude => 'alt\.anonymous\.messages', |
| score_exclude => 'alt\.anonymous\.messages', |
| |
| |
| ### MIME HTML allowed here (if block_mime_html is True) |
| ### MIME HTML allowed here (if block_mime_html is True) |
| mime_html_allowed => '^pgsql\.|^relcom\.|^gmane\.', |
| mime_html_allowed => '^pgsql\.|^relcom\.|^gmane\.', |
| |
| |
| ### HTML allowed here (if block_html is True) |
| ### HTML allowed here (if block_html is True) |
| html_allowed => "^relcom\.|^microsoft\.", |
| html_allowed => "^relcom\.|^microsoft\.", |
| |
| |
| test_groups => '\.test(ing)?(?:$|\.)|^es\.pruebas|^borland\.public\.test2'. |
| test_groups => '\.test(ing)?(?:$|\.)|^es\.pruebas|^borland\.public\.test2'. |
| '|^cern\.testnews', |
| '|^cern\.testnews', |
| |
| |
| ### groups where we restrict crossposts even more than normal |
| ### groups where we restrict crossposts even more than normal |
| low_xpost_groups => 'test|jobs|forsale', |
| low_xpost_groups => 'test|jobs|forsale', |
| |
| |
| ### Groups where we restrict crossposts with other groups as a result of |
| ### Groups where we restrict crossposts with other groups as a result of |
| ### the meow wars. (<4curm4$r1@decaxp.harvard.edu>) |
| ### the meow wars. (<4curm4$r1@decaxp.harvard.edu>) |
| meow_groups => '^alt\.fan\.karl-malden\.nose|^alt\.flame|^alt\.troll'. |
| meow_groups => '^alt\.fan\.karl-malden\.nose|^alt\.flame|^alt\.troll'. |
| '|^alt\.alien\.vampire\.flonk\.flonk\.flonk|^alt\.romath'. |
| '|^alt\.alien\.vampire\.flonk\.flonk\.flonk|^alt\.romath'. |
| '|^alt\.snuh|^alt\.fan\.natasha', |
| '|^alt\.snuh|^alt\.fan\.natasha', |
| |
| |
| ### Topic groups allow administrators to limit crossposting from defined |
| ### Topic groups allow administrators to limit crossposting from defined |
| ### groups to undefined groups. The allowed number of groups is defined |
| ### groups to undefined groups. The allowed number of groups is defined |
| ### in off_topic_maxgroups. |
| ### in off_topic_maxgroups. |
| ### Examples: '\.politi[ck]', '\.pets', '\.sex' |
| ### Examples: '\.politi[ck]', '\.pets', '\.sex' |
| topic1_groups => '', |
| topic1_groups => '', |
| topic2_groups => '', |
| topic2_groups => '', |
| |
| |
| ### cancel in these groups are not honored |
| ### cancel in these groups are not honored |
| no_cancel_groups => '^alt\.religion\.scientology|^news\.admin\.net-abuse|^alt\.config$', |
| no_cancel_groups => '^alt\.religion\.scientology|^news\.admin\.net-abuse|^alt\.config$', |
| |
| |
| ### domains starting/ending in "xxx" are never good news |
| ### domains starting/ending in "xxx" are never good news |
| ### (checked against .com, .net, and .nu tld's only) |
| ### (checked against .com, .net, and .nu tld's only) |
| # FIXME currently disabled |
| # FIXME currently disabled |
| # baddomainpat => '[\w\-]+xxx|xxx[\w\-]+', |
| # baddomainpat => '[\w\-]+xxx|xxx[\w\-]+', |
| |
| |
| ### Exclude these Newsgroups from the From / Subject / Lines filter |
| ### Exclude these Newsgroups from the From / Subject / Lines filter |
| fsl_exclude => 'comp\.lang\.ruby', |
| fsl_exclude => 'comp\.lang\.ruby', |
| |
| |
| ### Exclude these Newsgroups from the Posting-Host / Lines filter |
| ### Exclude these Newsgroups from the Posting-Host / Lines filter |
| phl_exclude => 'comp\.lang\.ruby|^microsoft\.|^alt\.bestjobsusa'. |
| phl_exclude => 'comp\.lang\.ruby|^microsoft\.|^alt\.bestjobsusa'. |
| '|\.bbs\.|^relcom\.hot-news|^szn\.news\.', |
| '|\.bbs\.|^relcom\.hot-news|^szn\.news\.', |
| |
| |
| ### Exclude these Newsgroups from the Posting-Host / Newsgroup filter |
| ### Exclude these Newsgroups from the Posting-Host / Newsgroup filter |
| phn_exclude => '^local\.|^alt\.anonymous\.messages'. |
| phn_exclude => '^local\.|^alt\.anonymous\.messages'. |
| '|^\w+\.bin|^microsoft\.|\.bbs\.|^alt\.bestjobsusa|^mozilla\.'. |
| '|^\w+\.bin|^microsoft\.|\.bbs\.|^alt\.bestjobsusa|^mozilla\.'. |
| '|^gnus?\.|^alt\.pictures\.|^gmane\.|^fa\.|^stu\.|^corel\.|\.cvs\.'. |
| '|^gnus?\.|^alt\.pictures\.|^gmane\.|^fa\.|^stu\.|^corel\.|\.cvs\.'. |
| '|\.talk|^lists\.|^microsoft\.|news\.lists\.filters|^perl\.'. |
| '|\.talk|^lists\.|^microsoft\.|news\.lists\.filters|^perl\.'. |
| '|\.marketplace|\.ebay|\.forsale|^relcom\.hot-news|^szn\.news\.'. |
| '|\.marketplace|\.ebay|\.forsale|^relcom\.hot-news|^szn\.news\.'. |
| '|^comp\.lang\.python', |
| '|^comp\.lang\.python', |
| |
| |
| # These hosts create unlinkable Posting-Host headers, rendering them |
| # These hosts create unlinkable Posting-Host headers, rendering them |
| # useless for hashing purposes. In these instances the NPH and PHR filter |
| # useless for hashing purposes. In these instances the NPH and PHR filter |
| # will use the Path header instead, (if phn_aggressive is true). The PHL |
| # will use the Path header instead, (if phn_aggressive is true). The PHL |
| # filter will ignore posts from these hosts. |
| # filter will ignore posts from these hosts. |
| bad_nph_hosts => 'newsguy\.com|tornevall\.net', |
| bad_nph_hosts => 'newsguy\.com|tornevall\.net', |
| |
| |
| ### Exempt these hosts from the Posting-Host / Lines filter |
| ### Exempt these hosts from the Posting-Host / Lines filter |
| phl_exempt => '^localhost$|webtv\.net$|^newscene\.newscene\.com$'. |
| phl_exempt => '^localhost$|webtv\.net$|^newscene\.newscene\.com$'. |
| '|^freebsd\.csie\.nctu\.edu\.tw$|^ddt\.demos\.su$|^onlyNews customer$'. |
| '|^freebsd\.csie\.nctu\.edu\.tw$|^ddt\.demos\.su$|^onlyNews customer$'. |
| '|localhost\.pld-linux\.org', |
| '|localhost\.pld-linux\.org', |
| |
| |
| ### Exempt these hosts from the Posting-Host / Newsgroup filter |
| ### Exempt these hosts from the Posting-Host / Newsgroup filter |
| phn_exempt => '^localhost$|^127\.0\.0\.1$|localhost\.pld-linux\.org', |
| phn_exempt => '^localhost$|^127\.0\.0\.1$|localhost\.pld-linux\.org', |
| |
| |
| ### Exempt these hosts from the High-Risk Newsgroups filter |
| ### Exempt these hosts from the High-Risk Newsgroups filter |
| phr_exempt => '^localhost$|^127\.0\.0\.1$', |
| phr_exempt => '^localhost$|^127\.0\.0\.1$', |
| |
| |
| ### Exclude these groups from the ratio based scoring filters. (Only used |
| ### Exclude these groups from the ratio based scoring filters. (Only used |
| ### if do_ratio_scoring is true). |
| ### if do_ratio_scoring is true). |
| ratio_exclude => '^cn\.|^tw\.|^japan\.|^fj\.|\.china', |
| ratio_exclude => '^cn\.|^tw\.|^japan\.|^fj\.|\.china', |
| |
| |
| ### Newsgroups that get frequently flooded. This defines the groups |
| ### Newsgroups that get frequently flooded. This defines the groups |
| ### processed by the PHR filter. It should be entirely user-defined. |
| ### processed by the PHR filter. It should be entirely user-defined. |
| flood_groups => '', |
| flood_groups => '', |
| |
| |
| ### posting hosts exempt from excessive supersedes filter |
| ### posting hosts exempt from excessive supersedes filter |
| supersedes_exempt => '^localhost$|^penguin-lust\.mit\.edu$', |
| supersedes_exempt => '^localhost$|^penguin-lust\.mit\.edu$', |
| |
| |
| ### refuse articles with these in the message-id (INN only) |
| ### refuse articles with these in the message-id (INN only) |
| refuse_messageids => 'HeadHunter\.NET>|none\d+\.yet>', |
| refuse_messageids => 'HeadHunter\.NET>|none\d+\.yet>', |
| |
| |
| ### groups expected to contain bodies and/or subject lines from spam |
| ### groups expected to contain bodies and/or subject lines from spam |
| spam_report_groups => '^(?:news|de)\.admin\.net-abuse'. |
| spam_report_groups => '^(?:news|de)\.admin\.net-abuse'. |
| '|news\.lists\.filters|\.nocem|\.spamtrap$|\.spam\.sightings'. |
| '|news\.lists\.filters|\.nocem|\.spamtrap$|\.spam\.sightings'. |
| '|^fr\.usenet\.abus\.rapports|^nl\.internet\.misbruik\.rapport$', |
| '|^fr\.usenet\.abus\.rapports|^nl\.internet\.misbruik\.rapport$', |
| |
| |
| adult_groups => 'personals|sex|nud[ei]|erot|xxx|lolita'. |
| adult_groups => 'personals|sex|nud[ei]|erot|xxx|lolita'. |
| '|neojapan|bondage|fetish|lesbian|porn|tasteless|voyeur|^it\.sesso'. |
| '|neojapan|bondage|fetish|lesbian|porn|tasteless|voyeur|^it\.sesso'. |
| '|^alt\.(?:mag[\.a]|redh|stories'. |
| '|^alt\.(?:mag[\.a]|redh|stories'. |
| '|fan\.(?:air|asp|pret|televisionx|pst|snuf))'. |
| '|fan\.(?:air|asp|pret|televisionx|pst|snuf))'. |
| '|^alt\.binaries\.(?:aimee|adole|ass\b|great|images\.(?:sun|under)|full'. |
| '|^alt\.binaries\.(?:aimee|adole|ass\b|great|images\.(?:sun|under)|full'. |
| '|linger|pent|pin-?up|nospam|scanm|pictures\.(?:aspa|bc|blon|blueb|bru'. |
| '|linger|pent|pin-?up|nospam|scanm|pictures\.(?:aspa|bc|blon|blueb|bru'. |
| '|centerf|coc|girlfr|horny|hussy|strip)|multimedia\.(?:boy|natur))', |
| '|centerf|coc|girlfr|horny|hussy|strip)|multimedia\.(?:boy|natur))', |
| |
| |
| not_adult_groups => 'sexual\.abuse|^soc.sex|^fr\.soc\.homosexualite'. |
| not_adult_groups => 'sexual\.abuse|^soc.sex|^fr\.soc\.homosexualite'. |
| '|^alt\.(?:support|teens|answers)', |
| '|^alt\.(?:support|teens|answers)', |
| |
| |
| faq_groups => '\.faqs?$|\.answers$|^news\.announce\.newgroups$'. |
| faq_groups => '\.faqs?$|\.answers$|^news\.announce\.newgroups$'. |
| '|^news\.admin\.hierarchies$', |
| '|^news\.admin\.hierarchies$', |
| |
| |
| local_approved_groups => 'alt\.|news\.admin\.net-abuse\.'. |
| local_approved_groups => 'alt\.|news\.admin\.net-abuse\.'. |
| '|fr\.misc\.bavardages\.dinosaures|alt\.sysadmin\.recovery'. |
| '|fr\.misc\.bavardages\.dinosaures|alt\.sysadmin\.recovery'. |
| '|alt\.tech-support\.recovery|alt\.dev\.null' |
| '|alt\.tech-support\.recovery|alt\.dev\.null' |
| |
| |
| ); |
| ); |
| |
| |
| # Store the Cleanfeed development revision number |
| # Store the Cleanfeed development revision number |
| #($version) = q$Revision$ =~ /(\d+)/; |
| #($version) = q$Revision$ =~ /(\d+)/; |
| #($version_date) = q$Date$ =~ /(\d{4}(\-\d{2}){2})/; |
| #($version_date) = q$Date$ =~ /(\d{4}(\-\d{2}){2})/; |
| |
| |
| ### List of group patterns that don't allow outside crossposts. |
| ### List of group patterns that don't allow outside crossposts. |
| ### Key is "friendly" name, value is the pattern. |
| ### Key is "friendly" name, value is the pattern. |
| %Restricted_Groups = ( |
| %Restricted_Groups = ( |
| cl => '^cl\.', |
| cl => '^cl\.', |
| net => '^net\.', |
| net => '^net\.', |
| bofh => '^bofh\.', |
| bofh => '^bofh\.', |
| 'de.alt.dateien' => '^de\.alt\.dateien', |
| 'de.alt.dateien' => '^de\.alt\.dateien', |
| sdnet => '^sdnet\.', # Requested by William Kronert |
| sdnet => '^sdnet\.', # Requested by William Kronert |
| ); |
| ); |
| |
| |
| # Load up the external config file |
| # Load up the external config file |
| my $local_file = "$config_dir/cleanfeed.local"; |
| my $local_file = "$config_dir/cleanfeed.local"; |
| $Local_Conf_Err = 0; |
| $Local_Conf_Err = 0; |
| if ($config_dir and -e $local_file) { |
| if ($config_dir and -e $local_file) { |
| undef %config_local; |
| undef %config_local; |
| undef %config_append; |
| undef %config_append; |
| if (open(CF, $local_file)) { |
| if (open(CF, $local_file)) { |
| my $cf = join('', <CF>); |
| my $cf = join('', <CF>); |
| close CF; |
| close CF; |
| eval $cf; |
| eval $cf; |
| if ($@) { |
| if ($@) { |
| slog('E', "Cannot load $local_file: $@"); |
| slog('E', "Cannot load $local_file: $@"); |
| $Local_Conf_Err = 1; |
| $Local_Conf_Err = 1; |
| } else { |
| } else { |
| local_config() if defined &local_config; |
| local_config() if defined &local_config; |
| } |
| } |
| } else { |
| } else { |
| slog('E', "Cannot open $local_file: $!"); |
| slog('E', "Cannot open $local_file: $!"); |
| $Local_Conf_Err = 1; |
| $Local_Conf_Err = 1; |
| } |
| } |
| |
| |
| # config_local overrides the config settings |
| # config_local overrides the config settings |
| if (%config_local) { |
| if (%config_local) { |
| $config{$_} = $config_local{$_} foreach keys %config_local; |
| $config{$_} = $config_local{$_} foreach keys %config_local; |
| undef %config_local; |
| undef %config_local; |
| } |
| } |
| # config_append adds to the config regexps |
| # config_append adds to the config regexps |
| if (%config_append) { |
| if (%config_append) { |
| foreach (qw(bin_allowed bad_bin md5exclude poison_groups |
| foreach (qw(bin_allowed bad_bin md5exclude poison_groups |
| allexclude html_allowed mime_html_allowed low_xpost_groups |
| allexclude html_allowed mime_html_allowed low_xpost_groups |
| test_groups no_cancel_groups baddomainpat fsl_exclude |
| test_groups no_cancel_groups baddomainpat fsl_exclude |
| phl_exempt phl_exclude supersedes_exempt bad_nph_hosts |
| phl_exempt phl_exclude supersedes_exempt bad_nph_hosts |
| phn_exempt phr_exempt phn_exclude flood_groups |
| phn_exempt phr_exempt phn_exclude flood_groups |
| refuse_messageids net_abuse_groups spam_report_groups |
| refuse_messageids net_abuse_groups spam_report_groups |
| adult_groups not_adult_groups faq_groups ratio_exclude |
| adult_groups not_adult_groups faq_groups ratio_exclude |
| image_allowed image_extensions meow_groups |
| image_allowed image_extensions meow_groups |
| topic1_groups topic2_groups local_approved_groups)) { |
| topic1_groups topic2_groups local_approved_groups)) { |
| if (defined $config_append{$_}) { |
| if (defined $config_append{$_}) { |
| $config{$_} .= "|$config_append{$_}"; |
| $config{$_} .= "|$config_append{$_}"; |
| $config{$_} =~ s/\|\|/\|/g; |
| $config{$_} =~ s/\|\|/\|/g; |
| } |
| } |
| $config{$_} =~ s/^\|//; |
| $config{$_} =~ s/^\|//; |
| $config{$_} =~ s/\|$//; |
| $config{$_} =~ s/\|$//; |
| } |
| } |
| undef %config_append; |
| undef %config_append; |
| } |
| } |
| } |
| } |
| |
| |
| |
| validate_configuration() if $config{validate_config};
|
| |
|
|
| @Restricted_List = keys %Restricted_Groups; |
| @Restricted_List = keys %Restricted_Groups; |
| |
| |
| # Create the logfile path. Will be undefined if logging is broken |
| # Create the logfile path. Will be undefined if logging is broken |
| if ($config{log_directory} and $config{log_name}) { |
| if ($config{log_directory} and $config{log_name}) { |
| $Log_File = "$config{log_directory}/$config{log_name}"; |
| $Log_File = "$config{log_directory}/$config{log_name}"; |
| } else { |
| } else { |
| undef $Log_File; |
| undef $Log_File; |
| } |
| } |
| |
| |
| # parse the active file if we've been given one. |
| # parse the active file if we've been given one. |
| if ($config{active_file}) { |
| if ($config{active_file}) { |
| %Moderated = (); |
| %Moderated = (); |
| if (open(ACTIVE, $config{active_file})) { |
| if (open(ACTIVE, $config{active_file})) { |
| while (<ACTIVE>) { |
| while (<ACTIVE>) { |
| chomp; |
| chomp; |
| my ($group, undef, undef, $flag) = split(/ /); |
| my ($group, undef, undef, $flag) = split(/ /); |
| $Moderated{$group} = 1 if $flag eq 'm'; |
| $Moderated{$group} = 1 if $flag eq 'm'; |
| } |
| } |
| close ACTIVE; |
| close ACTIVE; |
| } else { |
| } else { |
| slog('E', "Cannot open $config{active_file}: $!"); |
| slog('E', "Cannot open $config{active_file}: $!"); |
| } |
| } |
| } |
| } |
| |
| |
| # Try and load base64 decoding functionality. |
| # Try and load base64 decoding functionality. |
| eval "use MIME::Base64; 1" or $config{nobase64} = 1; |
| eval "use MIME::Base64; 1" or $config{nobase64} = 1; |
| |
| |
| } # end of get_config() |
| } # end of get_config() |
| |
| |
| # Regexps for matching URLs |
| # Regexps for matching URLs |
| $TLDs = '(?:[Cc][Oo][Mm]|[Nn][Ee][Tt]|[Oo][Rr][Gg]|[Ee][Dd][Uu]' . |
| $TLDs = '(?:[Cc][Oo][Mm]|[Nn][Ee][Tt]|[Oo][Rr][Gg]|[Ee][Dd][Uu]' . |
| '|[Cc][Oo]\.[Uu][Kk]|[Ff][Rr]' . |
| '|[Cc][Oo]\.[Uu][Kk]|[Ff][Rr]' . |
| '|[Cc][Oo][Mm]\.[Aa][Uu]|[Nn][Ll]|[Dd][Ee]|[Nn][Oo]|[Dd][Kk]|[Cc][Hh]' . |
| '|[Cc][Oo][Mm]\.[Aa][Uu]|[Nn][Ll]|[Dd][Ee]|[Nn][Oo]|[Dd][Kk]|[Cc][Hh]' . |
| '|[Ss][Ee]|[Nn][Uu]|[Tt][Oo]|[Rr][Uu]|[Uu][Aa]|[Cc][Aa]|[Cc][Xx])'; |
| '|[Ss][Ee]|[Nn][Uu]|[Tt][Oo]|[Rr][Uu]|[Uu][Aa]|[Cc][Aa]|[Cc][Xx])'; |
| #$IP = '\d\d\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?\b'; |
| #$IP = '\d\d\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?\b'; |
| $IP = '(?:\d{1,3}\.){3}\d{1,3}\b'; |
| $IP = '(?:\d{1,3}\.){3}\d{1,3}\b'; |
| $StealthIP = '(?:\d{10}|0[0-7]+\.0[0-7]+\.0[0-7]+\.0[0-7]+)'; |
| $StealthIP = '(?:\d{10}|0[0-7]+\.0[0-7]+\.0[0-7]+\.0[0-7]+)'; |
| # Make $WebHost only match if there's nothing before it (requires 5.005). |
| # Make $WebHost only match if there's nothing before it (requires 5.005). |
| $WebHost = ($] >= 5.005 ? '(?<![\w.])' : '' ) . |
| $WebHost = ($] >= 5.005 ? '(?<![\w.])' : '' ) . |
| '(?:[Ww][Ww][Ww]\d?|[Ww][Ee][Bb]\d?|[Mm][Ee][Mm][Bb][Ee][Rr][Ss]' . |
| '(?:[Ww][Ww][Ww]\d?|[Ww][Ee][Bb]\d?|[Mm][Ee][Mm][Bb][Ee][Rr][Ss]' . |
| '|[Uu][Ss][Ee][Rr][Ss]?|[Hh][Oo][Mm][Ee])'; |
| '|[Uu][Ss][Ee][Rr][Ss]?|[Hh][Oo][Mm][Ee])'; |
| $HTTP = '(?:www\.|https?:\/\/)'; |
| $HTTP = '(?:www\.|https?:\/\/)'; |
| $HOST = '[\w\-.]+'; # characters for the hostname |
| $HOST = '[\w\-.]+'; # characters for the hostname |
| $PORT = '(?::\d+)?'; # always optional |
| $PORT = '(?::\d+)?'; # always optional |
| $Hostname = '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9])'. |
| $Hostname = '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9])'. |
| '(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9]))+)'; |
| '(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9]))+)'; |
| |
| |
| $url = $HTTP.$HOST.'|'.$WebHost . $HOST .'\.'. "$TLDs|$HTTP(?:$IP|$StealthIP)"; |
| $url = $HTTP.$HOST.'|'.$WebHost . $HOST .'\.'. "$TLDs|$HTTP(?:$IP|$StealthIP)"; |
| # requires the http:// part but accepts any hostname or tld, or IP-based url. |
| # requires the http:// part but accepts any hostname or tld, or IP-based url. |
| $url2 = "$HTTP(?:$HOST|$IP|$StealthIP)"; |
| $url2 = "$HTTP(?:$HOST|$IP|$StealthIP)"; |
| # http:// is optional if $WebHost matches, any tld accepted. |
| # http:// is optional if $WebHost matches, any tld accepted. |
| $url3 = "(?:$HTTP|$WebHost)$HOST"; |
| $url3 = "(?:$HTTP|$WebHost)$HOST"; |
| $stealthURL = "$HTTP(?:$IP|$StealthIP)"; |
| $stealthURL = "$HTTP(?:$IP|$StealthIP)"; |
| $simpleURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?'; |
| $simpleURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?'; |
| # match the whole URL including path (used by body_urls() ) |
| # match the whole URL including path (used by body_urls() ) |
| $fullURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?'. |
| $fullURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?'. |
| "|(?:$WebHost\.$HOST\.$TLDs)$PORT" . '(?:\/[^\s<>]+)?'; |
| "|(?:$WebHost\.$HOST\.$TLDs)$PORT" . '(?:\/[^\s<>]+)?'; |
| $spacecom = 'www[^a-z\.]+[a-zA-Z0-9\-]+[^a-z\.]+com'; |
| $spacecom = 'www[^a-z\.]+[a-zA-Z0-9\-]+[^a-z\.]+com'; |
| # URL redirection services (Commonly used to obfuscate spam links). |
| # URL redirection services (Commonly used to obfuscate spam links). |
| #$shorturl = 'tinyurl\.com|useurl\.us|linkurl\.r8\.org|urlbrief\.com'. |
| #$shorturl = 'tinyurl\.com|useurl\.us|linkurl\.r8\.org|urlbrief\.com'. |
| # '|sites\.google\.com/site/|groups\.google\.com/group/'. |
| # '|sites\.google\.com/site/|groups\.google\.com/group/'. |
| # '|6x\.to|liteurl\.com|xurl\.jp|c4\.to|da\.ru|beam\.to|doiop\.com'. |
| # '|6x\.to|liteurl\.com|xurl\.jp|c4\.to|da\.ru|beam\.to|doiop\.com'. |
| # '|kickme\.to|end\.at|has\.it|hotshorturl\.com'; |
| # '|kickme\.to|end\.at|has\.it|hotshorturl\.com'; |
| |
| |
| # for the scoring filter |
| # for the scoring filter |
| $sex = 'sex|xxx|fuck'; |
| $sex = 'sex|xxx|fuck'; |
| $free = 'free(?!dom|bsd|ppp|xp)'; |
| $free = 'free(?!dom|bsd|ppp|xp)'; |
| $pics = 'pi(?:c|x)'; |
| $pics = 'pi(?:c|x)'; |
| $drugs = 'ativan|carisoprodol|caverta|\bcialis|diazepam|kamagra|levitra|nizoral'. |
| $drugs = 'ativan|carisoprodol|caverta|\bcialis|diazepam|kamagra|levitra|nizoral'. |
| 'sildenafil|tadalafil|valium|viagra|vicodin|xanax'; |
| 'sildenafil|tadalafil|valium|viagra|vicodin|xanax'; |
| # US Phone 800-123-1234 UK Phone 09096-400141 |
| # US Phone 800-123-1234 UK Phone 09096-400141 |
| $phone = '\b[89]\d{2}[\.\s\-_*\)]*\d{2,3}[\.\s\-_*]*\d{4}\b'. |
| $phone = '\b[89]\d{2}[\.\s\-_*\)]*\d{2,3}[\.\s\-_*]*\d{4}\b'. |
| '|[\s\-_*0][89]\d{2,3}[\s\-_*]+\d{6}'; |
| '|[\s\-_*0][89]\d{2,3}[\s\-_*]+\d{6}'; |
| $desc1 = "hard.?core|teen|asian|extreme|live|outrageous|nasty|awesome|$free|adult"; |
| $desc1 = "hard.?core|teen|asian|extreme|live|outrageous|nasty|awesome|$free|adult"; |
| # ASCII Symbols, excluding space/tab. |
| # ASCII Symbols, excluding space/tab. |
| $SYMBOL = '[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]'; |
| $SYMBOL = '[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]'; |
| $hws = '(?:[\t ])'; # Horizontal whitespace. (From Perl 5.10, this can be \h.) |
| $hws = '(?:[\t ])'; # Horizontal whitespace. (From Perl 5.10, this can be \h.) |
| $messageid_re = messageid_re(); |
| $messageid_re = messageid_re(); |
| |
| |
| # Re-enable the following when we fix the false positives in the scoring section. |
| # Re-enable the following when we fix the false positives in the scoring section. |
| #$site_desc = "$desc1|password"; |
| #$site_desc = "$desc1|password"; |
| #$servPre = "(?:$free|cheap|unlimited|nationwide|$site_desc)"; |
| #$servPre = "(?:$free|cheap|unlimited|nationwide|$site_desc)"; |
| #$servPost = '(?:$free|minute|samples|800|900|no.?charge)'; |
| #$servPost = '(?:$free|minute|samples|800|900|no.?charge)'; |
| #$servStr = "(?:phone.{0,15}(?:$sex|fun)|(?:adult|r.?a.?p.?e|$sex).{0,10}(?:chat|site)". |
| #$servStr = "(?:phone.{0,15}(?:$sex|fun)|(?:adult|r.?a.?p.?e|$sex).{0,10}(?:chat|site)". |
| # "|(?:$sex).{0,15}(?:show|call|connection|vid(?:eo|s)|dvd)". |
| # "|(?:$sex).{0,15}(?:show|call|connection|vid(?:eo|s)|dvd)". |
| # '|hard.?core.(?:vid(?:eo|s)|dvd|amateur)|900.dateline|(?:mass|bulk).e?-?mail)'; |
| # '|hard.?core.(?:vid(?:eo|s)|dvd|amateur)|900.dateline|(?:mass|bulk).e?-?mail)'; |
| #$services = "(?:$servPre.{0,30}?$servStr)|(?:$servStr.{0,30}?$servPost)"; |
| #$services = "(?:$servPre.{0,30}?$servStr)|(?:$servStr.{0,30}?$servPost)"; |
| |
| |
| $free_stuff = "$free.{0,20}(?:password|membership|$pics|chat)". |
| $free_stuff = "$free.{0,20}(?:password|membership|$pics|chat)". |
| "|(?:100\%|total|complete|absolut|all).{0,15}$free". |
| "|(?:100\%|total|complete|absolut|all).{0,15}$free". |
| '|no.{0,6}(a(?:ge|dult).(?:verification|check)|avs)'; |
| '|no.{0,6}(a(?:ge|dult).(?:verification|check)|avs)'; |
| |
| |
| $sex_adjs = "$desc1|$sex|erotic|gay|amateur|lesbian|blow.?job|fetish". |
| $sex_adjs = "$desc1|$sex|erotic|gay|amateur|lesbian|blow.?job|fetish". |
| '|pre.?teen|nude|celeb|school.?girl|bondage|rape|torture'; |
| '|pre.?teen|nude|celeb|school.?girl|bondage|rape|torture'; |
| $porn = "(?:$sex_adjs).{0,25}(?:$pics|video|dvd|image|porn|photo|mpeg)"; |
| $porn = "(?:$sex_adjs).{0,25}(?:$pics|video|dvd|image|porn|photo|mpeg)"; |
| |
| |
| $one_point_words = "pre.?teen|\bteen|\bsex|credit|amateur|horne?y". |
| $one_point_words = "pre.?teen|\bteen|\bsex|credit|amateur|horne?y". |
| '|anal(?!yst)|oral|bondage|breast|vid(?:eo|s)|dvd|kink(?:y|ier)|mistress'. |
| '|anal(?!yst)|oral|bondage|breast|vid(?:eo|s)|dvd|kink(?:y|ier)|mistress'. |
| '|orgy|erotic|porn|fetish|whore|nympho|sucking|password|membership'. |
| '|orgy|erotic|porn|fetish|whore|nympho|sucking|password|membership'. |
| '|make.money|fast.cash|orgasm|incest|extreme\b|raunchy|panties|bang'. |
| '|make.money|fast.cash|orgasm|incest|extreme\b|raunchy|panties|bang'. |
| '|taboo|muff\b|(?:young|cute|school).?girl|nasty|torture'. |
| '|taboo|muff\b|(?:young|cute|school).?girl|nasty|torture'. |
| 'suduc(?:e|tress)|cuckold|wicked|unlimited|masterbat'; |
| 'suduc(?:e|tress)|cuckold|wicked|unlimited|masterbat'; |
| $two_point_words = "$drugs". |
| $two_point_words = "$drugs". |
| '|fuck|sluts|puss(?:y|ies)|\bcum|(?:hidden|live|free|dorm|spy).?cam'. |
| '|fuck|sluts|puss(?:y|ies)|\bcum|(?:hidden|live|free|dorm|spy).?cam'. |
| '|le[sz]b(?:ian|o)|\btits?\b|dick(?!.?berg)|blow.?job|cock|clit'. |
| '|le[sz]b(?:ian|o)|\btits?\b|dick(?!.?berg)|blow.?job|cock|clit'. |
| '|twat|cunt|hard-?core|[^x]xxx|facial|gangbang|strap.on|submissive'. |
| '|twat|cunt|hard-?core|[^x]xxx|facial|gangbang|strap.on|submissive'. |
| '|(?:live|real|innocent).girl|phone.{0,5}(?:sex|fun|play)|she.?male'. |
| '|(?:live|real|innocent).girl|phone.{0,5}(?:sex|fun|play)|she.?male'. |
| '|lolita|dildo|whore|fingering|barely.?(?:18|legal)|[gm]ilf|no.?limit'. |
| '|lolita|dildo|whore|fingering|barely.?(?:18|legal)|[gm]ilf|no.?limit'. |
| '|uncensored|age.?play|pedo\b|pedophile'; |
| '|uncensored|age.?play|pedo\b|pedophile'; |
| |
| |
| # assorted spamware names found in X-Newreader/X-Mailer/etc headers |
| # assorted spamware names found in X-Newreader/X-Mailer/etc headers |
| $Xbot = '^2\.\d\.(?:\d\d? [a-z]|\d\d?)$|newsgroup bulk mailer' |
| $Xbot = '^2\.\d\.(?:\d\d? [a-z]|\d\d?)$|newsgroup bulk mailer' |
| . '|calvacade *98|atomicpost|uncle *spam' |
| . '|calvacade *98|atomicpost|uncle *spam' |
| . '|metanews \d|metapost|ng post|girlsdeluxe|usenet replayer' |
| . '|metanews \d|metapost|ng post|girlsdeluxe|usenet replayer' |
| . '|express news poster|^superpost auto marketer'; |
| . '|express news poster|^superpost auto marketer'; |
| |
| |
| ############################################################################## |
| ############################################################################## |
| |
| |
| get_config(); |
| get_config(); |
| setup_stuff(); |
| setup_stuff(); |
| |
| |
| # is this a reload? |
| # is this a reload? |
| if (defined $Start_Time) { |
| if (defined $Start_Time) { |
| writestats(1) if $MODE eq 'inn'; # write the stats file |
| writestats(1) if $MODE eq 'inn'; # write the stats file |
| } else { |
| } else { |
| restore_emp() if $config{do_emp_dump}; # load the saved state |
| restore_emp() if $config{do_emp_dump}; # load the saved state |
| $Start_Time = time; |
| $Start_Time = time; |
| } |
| } |
| |
| |
| |
| # Optional policy tables populated by cleanfeed.local.
|
| |
| # Keys are Perl regular expressions; values are hash references.
|
| |
| %Peer_Policies = () unless %Peer_Policies;
|
| |
| %Hierarchy_Policies = () unless %Hierarchy_Policies;
|
| |
| %policy_rule_count = ();
|
| |
| %policy_peer_count = ();
|
| |
| %policy_hierarchy_count = ();
|
| |
| %top_peer_reject = () unless %top_peer_reject;
|
| |
| %top_host_reject = () unless %top_host_reject;
|
| |
| %top_group_reject = () unless %top_group_reject;
|
| |
| %rate_peer = () unless %rate_peer;
|
| |
| %rate_host = () unless %rate_host;
|
| |
| %Bad_File_Mtime = () unless %Bad_File_Mtime;
|
| |
| $Last_Bad_Mtime_Check = 0 unless defined $Last_Bad_Mtime_Check;
|
| |
| $Config_Fingerprint = '' unless defined $Config_Fingerprint;
|
| |
| $Last_Metrics_CSV = time unless defined $Last_Metrics_CSV;
|
| |
|
|
| $Last_Trim = time unless defined $Last_Trim; |
| $Last_Trim = time unless defined $Last_Trim; |
| $Last_Stats = time unless defined $Last_Stats; |
| $Last_Stats = time unless defined $Last_Stats; |
| $Do_Log = 0; |
| $Do_Log = 0; |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # end of the initialization code |
| # end of the initialization code |
| ############################################################################## |
| ############################################################################## |
| |
| |
| # Set things up after we've got our configuration. |
| # Set things up after we've got our configuration. |
| |
| sub validate_configuration {
|
| |
| my @errors;
|
| |
|
|
| |
| my %boolean = map { $_ => 1 } qw(
|
| |
| verbose aggressive block_binaries block_all_binaries block_late_cancels
|
| |
| block_user_spamcancels block_user_cancels block_extra_reposts do_md5
|
| |
| do_phl do_phn do_phr do_fsl do_scoring_filter do_ratio_scoring
|
| |
| do_emp_dump fuzzy_md5 md5_skips_followups phn_aggressive phr_aggressive
|
| |
| do_mid_filter do_supersedes_filter drop_useless_controls drop_ihave_sendme
|
| |
| binaries_in_mod_groups block_mime_html block_html_multipart block_html
|
| |
| block_html_images log_accepts inn_syslog_status timer_info
|
| |
| detect_mime_binaries detect_malformed_yenc validate_config policy_enabled
|
| |
| policy_default_allow_binary policy_log_matches policy_log_accepts
|
| |
| policy_include_message_id policy_include_peer policy_include_groups
|
| |
| metrics_enabled metrics_syslog metrics_by_rule metrics_by_peer
|
| |
| metrics_by_hierarchy malformed_encoding_check binary_byte_profile_enabled
|
| |
| path_sanity_enabled anomaly_rate_enabled top_offenders_enabled
|
| |
| config_fingerprint_enabled idn_normalization external_regex_reject_unsafe
|
| |
| trusted_bypass_scoring trusted_bypass_content trusted_bypass_binary
|
| |
| trusted_bypass_crosspost trusted_bypass_size
|
| |
| );
|
| |
| for my $key (keys %boolean) {
|
| |
| push @errors, "$key must be 0 or 1"
|
| |
| if defined $config{$key} and $config{$key} !~ /\A[01]\z/;
|
| |
| }
|
| |
|
|
| |
| my %nonnegative = map { $_ => 1 } qw(
|
| |
| maxgroups bad_url_score trim_interval stats_interval MIDmaxlife
|
| |
| bad_rate_reload low_xpost_maxgroups meow_ext_maxgroups
|
| |
| off_topic1_maxgroups on_topic1_maxgroups on_topic1_mingroups
|
| |
| off_topic2_maxgroups on_topic2_maxgroups on_topic2_mingroups
|
| |
| max_base64_lines max_log_size keep_old_logs debug_batch_size
|
| |
| fuzzy_max_length md5_max_length binary_scan_bytes binary_scan_tail_bytes
|
| |
| body_preview_bytes study_max_lines MD5RateCutoff MD5RateCeiling
|
| |
| MD5RateBaseInterval PHLRateCutoff PHLRateCeiling PHLRateBaseInterval
|
| |
| PHNRateCutoff PHNRateCeiling PHNRateBaseInterval PHRRateCutoff
|
| |
| PHRRateCeiling PHRRateBaseInterval FSLRateCutoff FSLRateCeiling
|
| |
| FSLRateBaseInterval supersedes_window supersedes_ceiling supersedes_max_faq
|
| |
| supersedes_max_unmoderated supersedes_max_moderated
|
| |
| supersedes_max_all_moderated supersedes_max_unknown_active
|
| |
| policy_default_max_bytes policy_max_peer_counters
|
| |
| policy_max_hierarchy_counters metrics_csv_interval bad_reload_interval
|
| |
| external_regex_max_length external_regex_max_count external_regex_body_bytes
|
| |
| max_header_line_bytes max_body_line_bytes yenc_size_tolerance_percent
|
| |
| binary_nonprintable_ratio_percent binary_ratio_min_bytes binary_ratio_scan_bytes
|
| |
| path_max_hops path_max_token_bytes path_repeat_ceiling anomaly_window
|
| |
| anomaly_peer_articles anomaly_host_articles anomaly_max_keys
|
| |
| top_offenders_limit top_offenders_max_keys
|
| |
| );
|
| |
| for my $key (keys %nonnegative) {
|
| |
| push @errors, "$key must be a non-negative integer"
|
| |
| if defined $config{$key} and $config{$key} !~ /\A\d+\z/;
|
| |
| }
|
| |
|
|
| |
| for my $prefix (qw(MD5 PHL PHN PHR FSL)) {
|
| |
| my ($cut, $ceil) = @config{"${prefix}RateCutoff", "${prefix}RateCeiling"};
|
| |
| push @errors, "${prefix}RateCutoff must not exceed ${prefix}RateCeiling"
|
| |
| if defined $cut and defined $ceil and $cut > $ceil;
|
| |
| }
|
| |
|
|
| |
| my @regex_keys = qw(
|
| |
| bin_allowed image_allowed image_extensions bad_bin md5exclude
|
| |
| poison_groups allexclude score_exclude mime_html_allowed html_allowed
|
| |
| test_groups low_xpost_groups meow_groups topic1_groups topic2_groups
|
| |
| no_cancel_groups baddomainpat fsl_exclude phl_exclude phn_exclude
|
| |
| bad_nph_hosts phl_exempt phn_exempt phr_exempt ratio_exclude flood_groups
|
| |
| supersedes_exempt refuse_messageids spam_report_groups adult_groups
|
| |
| not_adult_groups faq_groups local_approved_groups
|
| |
| );
|
| |
| for my $key (@regex_keys) {
|
| |
| next if not defined $config{$key} or $config{$key} eq '';
|
| |
| my $ok = eval { qr/$config{$key}/; 1 };
|
| |
| push @errors, "$key contains an invalid regular expression: $@" if not $ok;
|
| |
| }
|
| |
| for my $key (keys %Restricted_Groups) {
|
| |
| my $ok = eval { qr/$Restricted_Groups{$key}/; 1 };
|
| |
| push @errors, "Restricted_Groups{$key} is invalid: $@" if not $ok;
|
| |
| }
|
| |
|
|
| |
|
|
| |
| for my $pair (
|
| |
| ['bad_reload_mode', qr/\A(?:mtime|articles|both)\z/],
|
| |
| ['long_line_mode', qr/\A(?:off|audit|reject)\z/],
|
| |
| ['malformed_encoding_mode', qr/\A(?:off|audit|reject)\z/],
|
| |
| ['binary_byte_profile_mode', qr/\A(?:off|audit|reject)\z/],
|
| |
| ['path_sanity_mode', qr/\A(?:off|audit|reject)\z/],
|
| |
| ['anomaly_mode', qr/\A(?:off|audit|reject)\z/],
|
| |
| ) {
|
| |
| my ($key, $valid) = @$pair;
|
| |
| push @errors, "$key contains an invalid mode" if defined $config{$key} and $config{$key} !~ $valid;
|
| |
| }
|
| |
| push @errors, 'binary_nonprintable_ratio_percent must be between 0 and 100'
|
| |
| if $config{binary_nonprintable_ratio_percent} > 100;
|
| |
|
|
| |
| push @errors, 'supersedes_mode must be off, audit, or reject'
|
| |
| if defined $config{supersedes_mode}
|
| |
| and $config{supersedes_mode} !~ /\A(?:off|audit|reject)\z/;
|
| |
|
|
| |
| push @errors, 'policy_mode must be off, audit, quarantine, or reject'
|
| |
| if defined $config{policy_mode}
|
| |
| and $config{policy_mode} !~ /\A(?:off|audit|quarantine|reject)\z/;
|
| |
|
|
| |
| for my $table_name (['Peer_Policies', \%Peer_Policies],
|
| |
| ['Hierarchy_Policies', \%Hierarchy_Policies]) {
|
| |
| my ($name, $table) = @$table_name;
|
| |
| for my $pattern (keys %$table) {
|
| |
| my $ok = eval { qr/$pattern/; 1 };
|
| |
| push @errors, "$name pattern '$pattern' is invalid: $@" if not $ok;
|
| |
| push @errors, "$name entry '$pattern' must be a hash reference"
|
| |
| if ref($table->{$pattern}) ne 'HASH';
|
| |
| next if ref($table->{$pattern}) ne 'HASH';
|
| |
| my $entry = $table->{$pattern};
|
| |
| push @errors, "$name entry '$pattern' mode is invalid"
|
| |
| if defined $entry->{mode}
|
| |
| and $entry->{mode} !~ /\A(?:off|audit|quarantine|reject)\z/;
|
| |
| push @errors, "$name entry '$pattern' max_bytes must be a non-negative integer"
|
| |
| if defined $entry->{max_bytes} and $entry->{max_bytes} !~ /\A\d+\z/;
|
| |
| push @errors, "$name entry '$pattern' allow_binary must be 0 or 1"
|
| |
| if defined $entry->{allow_binary} and $entry->{allow_binary} !~ /\A[01]\z/;
|
| |
| }
|
| |
| }
|
| |
|
|
| |
| if (@errors) {
|
| |
| $Local_Conf_Err = 1;
|
| |
| slog('E', "Configuration validation failed: $_") for @errors;
|
| |
| }
|
| |
| return @errors ? 0 : 1;
|
| |
| }
|
| |
|
|
| sub setup_stuff { |
| sub setup_stuff { |
| # Try to load up MD5 module (use Digest::MD5, but old MD5 still works). |
| # Try to load up MD5 module (use Digest::MD5, but old MD5 still works). |
| if ($config{do_md5}) { |
| if ($config{do_md5}) { |
| eval { require Digest::MD5; import Digest::MD5 qw(md5_hex); }; |
| eval { require Digest::MD5; import Digest::MD5 qw(md5_hex); }; |
| if ($@) { |
| if ($@) { |
| undef $config{do_md5}; |
| undef $config{do_md5}; |
| slog('E', 'Cannot load MD5: ' . $@); |
| slog('E', 'Cannot load MD5: ' . $@); |
| } |
| } |
| } else { |
| } else { |
| undef $config{do_md5}; |
| undef $config{do_md5}; |
| } |
| } |
| |
| |
| # Try to load up Data::Dumper if we want to save the EMP histories. |
| # Try to load up Data::Dumper if we want to save the EMP histories. |
| if ($config{do_emp_dump}) { |
| if ($config{do_emp_dump}) { |
| eval { require Data::Dumper; }; |
| eval { require Data::Dumper; }; |
| if ($@) { |
| if ($@) { |
| undef $config{do_emp_dump}; |
| undef $config{do_emp_dump}; |
| slog('E', 'Cannot load Data::Dumper: ' . $@); |
| slog('E', 'Cannot load Data::Dumper: ' . $@); |
| } |
| } |
| } |
| } |
| |
| |
| # Load up IO::File if we want logging. |
| # Load up IO::File if we want logging. |
| if ($Log_File) { |
| if ($Log_File) { |
| eval { require IO::File; }; |
| eval { require IO::File; }; |
| if ($@) { |
| if ($@) { |
| undef $Log_File; |
| undef $Log_File; |
| slog('E', 'Cannot load IO::File: ' . $@); |
| slog('E', 'Cannot load IO::File: ' . $@); |
| } |
| } |
| } |
| } |
| |
| |
| # Read all the bad_* files |
| # Read all the bad_* files |
| read_hashes(); |
| read_hashes(); |
| |
| |
| #Initialise status counters |
| #Initialise status counters |
| %status = (); |
| %status = (); |
| $status{accepted} = 0; |
| $status{accepted} = 0; |
| $status{rejected} = 0; |
| $status{rejected} = 0; |
| $status{refused} = 0; |
| $status{refused} = 0; |
| $status{do_mid_filter} = 0; |
| $status{do_mid_filter} = 0; |
| |
| $status{audited} = 0;
|
| |
| $status{quarantined} = 0;
|
| |
| |
| # initialise the rate filters |
| # initialise the rate filters |
| if ($config{do_md5}) { |
| if ($config{do_md5}) { |
| $MD5history = new Cleanfeed::RateLimit; |
| $MD5history = new Cleanfeed::RateLimit; |
| $MD5history->init($config{MD5RateCutoff}, $config{MD5RateCeiling}, |
| $MD5history->init($config{MD5RateCutoff}, $config{MD5RateCeiling}, |
| $config{MD5RateBaseInterval}); |
| $config{MD5RateBaseInterval}); |
| } else { |
| } else { |
| undef $MD5history; |
| undef $MD5history; |
| } |
| } |
| if ($config{do_phl}) { |
| if ($config{do_phl}) { |
| $PHLhistory = new Cleanfeed::RateLimit; |
| $PHLhistory = new Cleanfeed::RateLimit; |
| $PHLhistory->init($config{PHLRateCutoff}, $config{PHLRateCeiling}, |
| $PHLhistory->init($config{PHLRateCutoff}, $config{PHLRateCeiling}, |
| $config{PHLRateBaseInterval}); |
| $config{PHLRateBaseInterval}); |
| } else { |
| } else { |
| undef $PHLhistory; |
| undef $PHLhistory; |
| } |
| } |
| if ($config{do_phn}) { |
| if ($config{do_phn}) { |
| $PHNhistory = new Cleanfeed::RateLimit; |
| $PHNhistory = new Cleanfeed::RateLimit; |
| $PHNhistory->init($config{PHNRateCutoff}, $config{PHNRateCeiling}, |
| $PHNhistory->init($config{PHNRateCutoff}, $config{PHNRateCeiling}, |
| $config{PHNRateBaseInterval}); |
| $config{PHNRateBaseInterval}); |
| } else { |
| } else { |
| undef $PHNhistory; |
| undef $PHNhistory; |
| } |
| } |
| if ($config{do_phr}) { |
| if ($config{do_phr}) { |
| $PHRhistory = new Cleanfeed::RateLimit; |
| $PHRhistory = new Cleanfeed::RateLimit; |
| $PHRhistory->init($config{PHRRateCutoff}, $config{PHRRateCeiling}, |
| $PHRhistory->init($config{PHRRateCutoff}, $config{PHRRateCeiling}, |
| $config{PHRRateBaseInterval}); |
| $config{PHRRateBaseInterval}); |
| } else { |
| } else { |
| undef $PHRhistory; |
| undef $PHRhistory; |
| } |
| } |
| if ($config{do_fsl}) { |
| if ($config{do_fsl}) { |
| $FSLhistory = new Cleanfeed::RateLimit; |
| $FSLhistory = new Cleanfeed::RateLimit; |
| $FSLhistory->init($config{FSLRateCutoff}, $config{FSLRateCeiling}, |
| $FSLhistory->init($config{FSLRateCutoff}, $config{FSLRateCeiling}, |
| $config{FSLRateBaseInterval}); |
| $config{FSLRateBaseInterval}); |
| } else { |
| } else { |
| undef $FSLhistory; |
| undef $FSLhistory; |
| } |
| } |
| if ($config{do_supersedes_filter}) { |
| if ($config{do_supersedes_filter}) { |
| $Suphistory = new Cleanfeed::RateLimit; |
| $Suphistory = new Cleanfeed::RateLimit; |
| $Suphistory->init(0, 50, 900); |
| $Suphistory->init(0, $config{supersedes_ceiling}, $config{supersedes_window}); |
| } |
| } |
| |
| |
| $MIDhistory = new Cleanfeed::Queue; |
| $MIDhistory = new Cleanfeed::Queue; |
| $MIDhistory->maxlife($config{MIDmaxlife} * 3600) if $config{MIDmaxlife}; |
| $MIDhistory->maxlife($config{MIDmaxlife} * 3600) if $config{MIDmaxlife}; |
| |
| |
| $timer{time} = time if $config{timer_info} and not $timer{time}; |
| $timer{time} = time if $config{timer_info} and not $timer{time}; |
| } |
| } |
| |
| |
| sub filter_art { |
| sub filter_art { |
| $now = time; |
| $now = time; |
| undef $body; |
| undef $body; |
| undef $score; # String representation of article score |
| undef $score; # String representation of article score |
| %state = (); # Initialize our state hash |
| %state = (); # Initialize our state hash |
| ##### State Keys ##### |
| ##### State Keys ##### |
| # body_has_url; Bool: True if body contains a URL |
| # body_has_url; Bool: True if body contains a URL |
| # file_extension; Str: File extension on yEnc or UUencoded binaries |
| # file_extension; Str: File extension on yEnc or UUencoded binaries |
| # cache_is_binary; Flag: True if content is Binary |
| # cache_is_binary; Flag: True if content is Binary |
| # localfeed; Flag: True if article source is local |
| # localfeed; Flag: True if article source is local |
| # spamsource; Flag: True if article origin is spam friendly |
| # spamsource; Flag: True if article origin is spam friendly |
| # xreader; Str: Newsreader application, if specified |
| # xreader; Str: Newsreader application, if specified |
| # scoreval; Int: Numeric representation of article score |
| # scoreval; Int: Numeric representation of article score |
| # urlcount; Int: Number of URL's in an article |
| # urlcount; Int: Number of URL's in an article |
| # charset; Str: Article charset (if defined in Content-Type) |
| # charset; Str: Article charset (if defined in Content-Type) |
| # posting_host: Str: Posting host/address |
| # posting_host: Str: Posting host/address |
| # injection_host Str: Injection host |
| # injection_host Str: Injection host |
| # grpcnt: Int: Count of groups in Newsgroups header |
| # grpcnt: Int: Count of groups in Newsgroups header |
| # fupcnt Int: Count of groups in Followup-To header |
| # fupcnt Int: Count of groups in Followup-To header |
| # grpfupcnt: Int: Count of combined grpcnt and fupcnt (Unique) |
| # grpfupcnt: Int: Count of combined grpcnt and fupcnt (Unique) |
| # lines Int: Number of lines reported by inn |
| # lines Int: Number of lines reported by inn |
| # letters Int: Number of letters in the post [a-zA-Z] |
| # letters Int: Number of letters in the post [a-zA-Z] |
| # uppercase Int: Number of uppercase letters in the post [A-Z] |
| # uppercase Int: Number of uppercase letters in the post [A-Z] |
| # symbols Int: Number of symbols [!"#$%&'()*+,-:;=?@{|}~] |
| # symbols Int: Number of symbols [!"#$%&'()*+,-:;=?@{|}~] |
| |
| |
| $status{articles}++; |
| $status{articles}++; |
| $timer{articles}++ if $config{timer_info}; |
| $timer{articles}++ if $config{timer_info}; |
| |
| |
| # count the lines in the article - late-model INN does this for us. |
| # count the lines in the article - late-model INN does this for us. |
| if (defined $hdr{__LINES__}) { |
| if (defined $hdr{__LINES__}) { |
| $state{lines} = $hdr{__LINES__}; |
| $state{lines} = $hdr{__LINES__}; |
| } else { |
| } else { |
| $state{lines} = ($hdr{__BODY__} =~ tr/\n//); |
| $state{lines} = ($hdr{__BODY__} =~ tr/\n//); |
| }; |
| }; |
| $lines = $state{lines}; # TODO Remove after grace period. |
| $lines = $state{lines}; # Legacy alias retained for local hooks. |
| |
| |
| # Study the message BODY. This used to be in the local config file but |
| # Study the message BODY. This used to be in the local config file but |
| # this seems more logical. |
| # this seems more logical. |
| # TODO: Make the number of lines configurable.
|
| |
| if (not $config{nobase64} |
| if (not $config{nobase64} |
| and $hdr{'Content-Transfer-Encoding'} =~ /^base64$/i |
| and $hdr{'Content-Transfer-Encoding'} =~ /^base64$/i |
| and $hdr{'Content-Type'} =~ /^text/i) { |
| and $hdr{'Content-Type'} =~ /^text/i) { |
| $body = lc substr(MIME::Base64::decode($hdr{__BODY__}), 0, 4000); |
| $body = lc substr(MIME::Base64::decode($hdr{__BODY__} || ''), 0, $config{body_preview_bytes}); |
| } else { |
| } else { |
| $body = lc substr($hdr{__BODY__}, 0, 4000); |
| $body = lc substr($hdr{__BODY__} || '', 0, $config{body_preview_bytes}); |
| study $hdr{__BODY__} if $state{lines} <= 250; |
| study $hdr{__BODY__} if $config{study_max_lines} |
| |
| and $state{lines} <= $config{study_max_lines};
|
| }; |
| }; |
| |
| |
| # Reload the bad_* files every $bad_rate_reload articles accepted |
| maybe_reload_bad_files(); |
| if ($status{accepted} > 0 and $config{bad_rate_reload} > 0
|
| |
| and $status{accepted} % $config{bad_rate_reload} == 0
|
| |
| and $status{accepted} > $status{bad_reloaded}) {
|
| |
| slog('N', "Reloading bad files after $status{accepted} articles");
|
| |
| read_hashes();
|
| |
| # Prevent looping whilst waiting for another accepted article
|
| |
| $status{bad_reloaded} = $status{accepted};
|
| |
| };
|
| |
| |
| |
| # Try and ascertain the source news service. |
| # Try and ascertain the source news service. |
| if ($hdr{'Injection-Info'} =~ /^$hws*($Hostname)[ \t;]/) { |
| if ($hdr{'Injection-Info'} =~ /^$hws*($Hostname)[ \t;]/) { |
| $state{injection_host} = "$1" |
| $state{injection_host} = "$1" |
| } elsif ($hdr{'X-Trace'} =~ /^$hws*($Hostname)$hws/) { |
| } elsif ($hdr{'X-Trace'} =~ /^$hws*($Hostname)$hws/) { |
| $state{injection_host} = "$1" |
| $state{injection_host} = "$1" |
| } else { |
| } else { |
| $state{injection_host} = first_path_host($hdr{Path}); |
| $state{injection_host} = first_path_host($hdr{Path}); |
| }; |
| }; |
| |
| |
| # Try and ascertain the Posting-Host info |
| # Try and ascertain the Posting-Host info |
| if ($hdr{'Injection-Info'} =~ /posting-host$hws*=$hws*"?([^";]+)/) { |
| if ($hdr{'Injection-Info'} =~ /posting-host$hws*=$hws*"?([^";]+)/) { |
| $state{posting_host} = "$1"; |
| $state{posting_host} = "$1"; |
| } elsif ($hdr{'NNTP-Posting-Host'}) { |
| } elsif ($hdr{'NNTP-Posting-Host'}) { |
| $state{posting_host} = $hdr{'NNTP-Posting-Host'}; |
| $state{posting_host} = $hdr{'NNTP-Posting-Host'}; |
| } else { |
| } else { |
| $state{posting_host} = 0; |
| $state{posting_host} = 0; |
| }; |
| }; |
| |
| $state{posting_host} = normalize_domain($state{posting_host}) if $state{posting_host};
|
| |
| $state{injection_host} = normalize_domain($state{injection_host}) if $state{injection_host};
|
| |
| |
| # Provide a user-defined function for tagging posts that are fed from |
| # Provide a user-defined function for tagging posts that are fed from |
| # sources the operator considers local. Note: This includes, but is |
| # sources the operator considers local. Note: This includes, but is |
| # not limited to articles received via nnrpd. |
| # not limited to articles received via nnrpd. |
| if (defined &local_flag_localfeed) { |
| if (defined &local_flag_localfeed) { |
| $state{localfeed} = local_flag_localfeed(); |
| $state{localfeed} = local_flag_localfeed(); |
| }; |
| }; |
| |
| |
| # Provide a user-defined function for tagging posts that are fed from |
| # Provide a user-defined function for tagging posts that are fed from |
| # sources the operator considers spam friendly. These could easily be |
| # sources the operator considers spam friendly. These could easily be |
| # hardcoded but doing so might get me kneecapped. |
| # hardcoded but doing so might get me kneecapped. |
| if (defined &local_flag_spamsource) { |
| if (defined &local_flag_spamsource) { |
| $state{spamsource} = local_flag_spamsource(); |
| $state{spamsource} = local_flag_spamsource(); |
| }; |
| }; |
| |
| |
| # break out newsgroups into an array |
| # break out newsgroups into an array |
| @groups = sort(split(/[,\s]+/, $hdr{Newsgroups})); |
| @groups = sort(split(/[,\s]+/, $hdr{Newsgroups})); |
| if ($hdr{'Followup-To'}) { |
| if ($hdr{'Followup-To'}) { |
| @followups = split(/[,\s]+/, $hdr{'Followup-To'}); |
| @followups = split(/[,\s]+/, $hdr{'Followup-To'}); |
| } else { |
| } else { |
| @followups = @groups; |
| @followups = @groups; |
| }; |
| }; |
| |
| |
| # Produce a Newsgroups string sorted alphanumerically. This is |
| # Produce a Newsgroups string sorted alphanumerically. This is |
| # useful for filters on Newsgroups where spammers manipulate the |
| # useful for filters on Newsgroups where spammers manipulate the |
| # distribution in order to generate non-colliding hashes. |
| # distribution in order to generate non-colliding hashes. |
| my $sortgrps = join(',', @groups); |
| my $sortgrps = join(',', @groups); |
| |
| |
| # Create a merged array of groups and followups with no duplicates. |
| # Create a merged array of groups and followups with no duplicates. |
| # This is useful in instances where we consider follow-up groups to be |
| # This is useful in instances where we consider follow-up groups to be |
| # part of the distribution. |
| # part of the distribution. |
| my %merge = (); |
| my %merge = (); |
| my @grpfup = grep { ! $merge{ $_ }++ } (@groups, @followups); |
| my @grpfup = grep { ! $merge{ $_ }++ } (@groups, @followups); |
| |
| |
| # Count the number of groups in the distribution |
| # Count the number of groups in the distribution |
| $state{grpcnt} = scalar @groups; |
| $state{grpcnt} = scalar @groups; |
| $state{fupcnt} = scalar @followups; |
| $state{fupcnt} = scalar @followups; |
| $state{grpfupcnt} = scalar @grpfup; |
| $state{grpfupcnt} = scalar @grpfup; |
| |
| $state{article_bytes} = length($hdr{__BODY__} || '');
|
| |
| $state{peer} = policy_peer_identity();
|
| |
|
|
| |
| my $guard_result = apply_lightweight_guards();
|
| |
| return $guard_result if defined $guard_result and $guard_result ne '';
|
| |
|
|
| |
| if ($config{policy_enabled}) {
|
| |
| my $policy_result = apply_policy_engine();
|
| |
| return $policy_result if defined $policy_result and $policy_result ne '';
|
| |
| }
|
| |
| |
| trimhashes() if $now - $Last_Trim >= $config{trim_interval}; |
| trimhashes() if $now - $Last_Trim >= $config{trim_interval}; |
| writestats() if $now - $Last_Stats >= $config{stats_interval}; |
| writestats() if $now - $Last_Stats >= $config{stats_interval}; |
| |
| |
| # Check out the Newsgroups to which the article is posted |
| # Check out the Newsgroups to which the article is posted |
| %gr = (); |
| %gr = (); |
| for (@groups) { |
| for (@groups) { |
| foreach my $item (@Restricted_List) { |
| foreach my $item (@Restricted_List) { |
| $gr{'rg_'.$item}++ if /$Restricted_Groups{$item}/; |
| $gr{'rg_'.$item}++ if /$Restricted_Groups{$item}/; |
| } |
| } |
| $gr{binary}++ if $config{bin_allowed} and /$config{bin_allowed}/o; |
| $gr{binary}++ if $config{bin_allowed} and /$config{bin_allowed}/o; |
| $gr{image}++ if $config{image_allowed} and /$config{image_allowed}/o; |
| $gr{image}++ if $config{image_allowed} and /$config{image_allowed}/o; |
| $gr{bad_bin}++ if $config{bad_bin} and /$config{bad_bin}/o; |
| $gr{bad_bin}++ if $config{bad_bin} and /$config{bad_bin}/o; |
| $gr{html}++ if $config{html_allowed} and /$config{html_allowed}/o; |
| $gr{html}++ if $config{html_allowed} and /$config{html_allowed}/o; |
| $gr{mime_html}++ if $config{mime_html_allowed} |
| $gr{mime_html}++ if $config{mime_html_allowed} |
| and /$config{mime_html_allowed}/o; |
| and /$config{mime_html_allowed}/o; |
| $gr{poison}++ if $config{poison_groups} |
| $gr{poison}++ if $config{poison_groups} |
| and /$config{poison_groups}/o; |
| and /$config{poison_groups}/o; |
| $gr{reports}++ if $config{spam_report_groups} |
| $gr{reports}++ if $config{spam_report_groups} |
| and /$config{spam_report_groups}/o; |
| and /$config{spam_report_groups}/o; |
| $gr{no_cancel}++ if $config{no_cancel_groups} |
| $gr{no_cancel}++ if $config{no_cancel_groups} |
| and /$config{no_cancel_groups}/o; |
| and /$config{no_cancel_groups}/o; |
| $gr{test}++ if /$config{test_groups}/o; |
| $gr{test}++ if /$config{test_groups}/o; |
| $gr{adult}++ if /$config{adult_groups}/o |
| $gr{adult}++ if /$config{adult_groups}/o |
| and not /$config{not_adult_groups}/o; |
| and not /$config{not_adult_groups}/o; |
| $gr{faq}++ if /$config{faq_groups}/o; |
| $gr{faq}++ if /$config{faq_groups}/o; |
| $gr{ratio}++ if /$config{ratio_exclude}/o; |
| $gr{ratio}++ if /$config{ratio_exclude}/o; |
| $gr{approved}++ if /$config{local_approved_groups}/o; |
| $gr{approved}++ if /$config{local_approved_groups}/o; |
| if ($config{active_file}) { |
| if ($config{active_file}) { |
| $gr{mod}++ if $Moderated{$_}; |
| $gr{mod}++ if $Moderated{$_}; |
| } elsif (defined &INN::newsgroup) { |
| } elsif (defined &INN::newsgroup) { |
| $gr{mod}++ if INN::newsgroup($_) eq 'm'; |
| $gr{mod}++ if INN::newsgroup($_) eq 'm'; |
| }; |
| }; |
| |
| |
| }; |
| }; |
| |
| |
| # As above but check Newsgroups and Followup-To headers |
| # As above but check Newsgroups and Followup-To headers |
| for (@grpfup) { |
| for (@grpfup) { |
| $gr{skip}++ if $config{allexclude} and /$config{allexclude}/o; |
| $gr{skip}++ if $config{allexclude} and /$config{allexclude}/o; |
| $gr{fslskip}++ if $config{fslexclude} and /$config{fslexclude}/o; |
| $gr{fslskip}++ if $config{fsl_exclude} and /$config{fsl_exclude}/o; |
| $gr{md5skip}++ if $config{md5exclude} and /$config{md5exclude}/o; |
| $gr{md5skip}++ if $config{md5exclude} and /$config{md5exclude}/o; |
| $gr{phnskip}++ if $config{phn_exclude} and /$config{phn_exclude}/o; |
| $gr{phnskip}++ if $config{phn_exclude} and /$config{phn_exclude}/o; |
| $gr{phlskip}++ if $config{phl_exclude} and /$config{phl_exclude}/o; |
| $gr{phlskip}++ if $config{phl_exclude} and /$config{phl_exclude}/o; |
| $gr{scoreskip}++ if $config{score_exclude} |
| $gr{scoreskip}++ if $config{score_exclude} |
| and /$config{score_exclude}/o; |
| and /$config{score_exclude}/o; |
| $gr{phrinc}++ if $config{flood_groups} and /$config{flood_groups}/o; |
| $gr{phrinc}++ if $config{flood_groups} and /$config{flood_groups}/o; |
| $gr{low_xpost}++ if $config{low_xpost_groups} |
| $gr{low_xpost}++ if $config{low_xpost_groups} |
| and /$config{low_xpost_groups}/o; |
| and /$config{low_xpost_groups}/o; |
| $gr{meow}++ if $config{meow_groups} |
| $gr{meow}++ if $config{meow_groups} |
| and /$config{meow_groups}/o; |
| and /$config{meow_groups}/o; |
| $gr{topic1}++ if $config{topic1_groups} |
| $gr{topic1}++ if $config{topic1_groups} |
| and /$config{topic1_groups}/o; |
| and /$config{topic1_groups}/o; |
| $gr{topic2}++ if $config{topic2_groups} |
| $gr{topic2}++ if $config{topic2_groups} |
| and /$config{topic2_groups}/o; |
| and /$config{topic2_groups}/o; |
| $gr{localhier}++ if /^local\./; |
| $gr{localhier}++ if /^local\./; |
| }; |
| }; |
| |
| |
| # These only count if all Newsgroups match |
| # These only count if all Newsgroups match |
| $gr{image} = (($gr{image} + $gr{binary}) >= $state{grpcnt}); |
| $gr{image} = (($gr{image} + $gr{binary}) >= $state{grpcnt}); |
| $gr{binary} = ($gr{binary} == $state{grpcnt}); |
| $gr{binary} = ($gr{binary} == $state{grpcnt}); |
| $gr{reports} = ($gr{reports} == $state{grpcnt}); |
| $gr{reports} = ($gr{reports} == $state{grpcnt}); |
| $gr{binary} = 0 if $gr{bad_bin}; |
| $gr{binary} = 0 if $gr{bad_bin}; |
| $gr{html} = ($gr{html} == $state{grpcnt}); |
| $gr{html} = ($gr{html} == $state{grpcnt}); |
| $gr{mime_html} = ($gr{mime_html} == $state{grpcnt}); |
| $gr{mime_html} = ($gr{mime_html} == $state{grpcnt}); |
| $gr{allmod} = ($gr{mod} == $state{grpcnt}); |
| $gr{allmod} = ($gr{mod} == $state{grpcnt}); |
| $gr{alltest} = ($gr{test} == $state{grpcnt}); |
| $gr{alltest} = ($gr{test} == $state{grpcnt}); |
| $gr{alladult} = ($gr{adult} == $state{grpcnt}); |
| $gr{alladult} = ($gr{adult} == $state{grpcnt}); |
| $gr{ratio} = ($gr{ratio} == $state{grpcnt}); |
| $gr{ratio} = ($gr{ratio} == $state{grpcnt}); |
| $gr{approved} = ($gr{approved} == $state{grpcnt}); |
| $gr{approved} = ($gr{approved} == $state{grpcnt}); |
| |
| |
| # Same as above but include Followup_To in addition to Newsgroups |
| # Same as above but include Followup_To in addition to Newsgroups |
| $gr{skip} = ($gr{skip} == $state{grpfupcnt}); |
| $gr{skip} = ($gr{skip} == $state{grpfupcnt}); |
| $gr{fslskip} = ($gr{fslskip} == $state{grpfupcnt}); |
| $gr{fslskip} = ($gr{fslskip} == $state{grpfupcnt}); |
| $gr{md5skip} = ($gr{md5skip} == $state{grpfupcnt}); |
| $gr{md5skip} = ($gr{md5skip} == $state{grpfupcnt}); |
| $gr{phnskip} = ($gr{phnskip} == $state{grpfupcnt}); |
| $gr{phnskip} = ($gr{phnskip} == $state{grpfupcnt}); |
| $gr{phlskip} = ($gr{phlskip} == $state{grpfupcnt}); |
| $gr{phlskip} = ($gr{phlskip} == $state{grpfupcnt}); |
| $gr{scoreskip} = ($gr{scoreskip} == $state{grpfupcnt}); |
| $gr{scoreskip} = ($gr{scoreskip} == $state{grpfupcnt}); |
| $gr{alllocal} = ($gr{localhier} == $state{grpfupcnt}); |
| $gr{alllocal} = ($gr{localhier} == $state{grpfupcnt}); |
| |
| |
| # If all newsgroups are excluded from filtering, bail now |
| # If all newsgroups are excluded from filtering, bail now |
| return '' if $gr{skip}; |
| return '' if $gr{skip}; |
| |
| |
| foreach (@Restricted_List) { |
| foreach (@Restricted_List) { |
| $gr{'rg_'.$_.'_only'} = ($gr{'rg_'.$_} == $state{grpcnt}); |
| $gr{'rg_'.$_.'_only'} = ($gr{'rg_'.$_} == $state{grpcnt}); |
| } |
| } |
| |
| |
| if ($hdr{'Content-Type'} =~ /charset="?([^";\s]+)/io) { |
| if ($hdr{'Content-Type'} =~ /charset="?([^";\s]+)/io) { |
| $state{charset} = lc($1); |
| $state{charset} = lc($1); |
| }; |
| }; |
| |
| |
| # checks common to all article types ##################################### |
| # checks common to all article types ##################################### |
| return reject("Bad host ($state{posting_host})", 'Bad site') |
| return reject("Bad host ($state{posting_host})", 'Bad site') |
| if exists $Bad_Hosts{$state{posting_host}} |
| if exists $Bad_Hosts{$state{posting_host}} |
| or exists $Bad_Hosts_Central{$state{posting_host}}; |
| or exists $Bad_Hosts_Central{$state{posting_host}}; |
| |
| |
| @Path_Entries = split(/!/, $hdr{Path}); |
| @Path_Entries = split(/!/, $hdr{Path}); |
| foreach (@Path_Entries) { |
| foreach (@Path_Entries) { |
| return reject("Bad path ($_)", 'Bad site') if exists $Bad_Path{$_}; |
| return reject("Bad path ($_)", 'Bad site') if exists $Bad_Path{$_}; |
| } |
| } |
| |
| |
| # check for the most simple newsagent variations |
| # check for the most simple newsagent variations |
| if ($hdr{'Message-ID'} =~ |
| if ($hdr{'Message-ID'} =~ |
| /^< |
| /^< |
| (?:cancel\.)* |
| (?:cancel\.)* |
| [0-9A-F]{8,15}\.[a-z]{4,11} |
| [0-9A-F]{8,15}\.[a-z]{4,11} |
| \@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com) |
| \@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com) |
| >$/x) { |
| >$/x) { |
| if ($hdr{'X-Cancelled-By'}) { |
| if ($hdr{'X-Cancelled-By'}) { |
| return reject('Cancel for rejected article'); |
| return reject('Cancel for rejected article'); |
| } else { |
| } else { |
| return reject('NewsAgent', 'Bot signature'); |
| return reject('NewsAgent', 'Bot signature'); |
| } |
| } |
| } |
| } |
| |
| |
| # This check should only trap articles in inn < 2.6. From that time, |
| # This check should only trap articles in inn < 2.6. From that time, |
| # Message-ID validation to RFC5536 is integrated. |
| # Message-ID validation to RFC5536 is integrated. |
| if ($hdr{'Message-ID'} !~ $messageid_re) { |
| if ($hdr{'Message-ID'} !~ $messageid_re) { |
| return reject('Bad Message-ID', 'Invalid Header'); |
| return reject('Bad Message-ID', 'Invalid Header'); |
| }; |
| }; |
| |
| |
| return reject('NewsAgent (Path)') |
| return reject('NewsAgent (Path)') |
| if $hdr{Path} =~ /\.(?:posted|mismatch)$/; |
| if $hdr{Path} =~ /\.(?:posted|mismatch)$/; |
| |
| |
| # regular articles ####################################################### |
| # regular articles ####################################################### |
| if (not $hdr{Control}) { |
| if (not $hdr{Control}) { |
| # Good Usenet lines are terminated with CRLF |
| # Good Usenet lines are terminated with CRLF |
| $state{badlines} = 0; |
| $state{badlines} = 0; |
| $state{badlines}++ while $hdr{__BODY__} =~ /[^\r]\n/g; |
| $state{badlines}++ while $hdr{__BODY__} =~ /[^\r]\n/g; |
| # lowercase some headers for later |
| # lowercase some headers for later |
| undef %lch; |
| undef %lch; |
| $lch{from} = lc $hdr{From} |
| $lch{from} = lc $hdr{From} |
| || return reject('Malformed article'); |
| || return reject('Malformed article'); |
| $lch{subject} = lc $hdr{Subject} |
| $lch{subject} = lc $hdr{Subject} |
| || return reject('Malformed article'); |
| || return reject('Malformed article'); |
| $lch{'message-id'} = lc $hdr{'Message-ID'} |
| $lch{'message-id'} = lc $hdr{'Message-ID'} |
| || return reject('Malformed article'); |
| || return reject('Malformed article'); |
| $lch{sender} = lc $hdr{Sender} || ''; |
| $lch{sender} = lc $hdr{Sender} || ''; |
| $lch{organization} = lc $hdr{Organization} || ''; |
| $lch{organization} = lc $hdr{Organization} || ''; |
| $lch{'content-type'}= lc $hdr{'Content-Type'} || ''; |
| $lch{'content-type'}= lc $hdr{'Content-Type'} || ''; |
| |
| |
| if (defined &local_filter_first) { |
| if (defined &local_filter_first) { |
| my @result = local_filter_first(); |
| my @result = local_filter_first(); |
| return reject(@result) if $result[0]; |
| return reject(@result) if $result[0]; |
| } |
| } |
| |
| |
| # first thing, handle reposts ######################################## |
| # first thing, handle reposts ######################################## |
| if ($config{block_extra_reposts} and $hdr{Subject} =~ /^REPOST: / |
| if ($config{block_extra_reposts} and $hdr{Subject} =~ /^REPOST: / |
| and $hdr{Path} =~ /!resurrector!/) { |
| and $hdr{Path} =~ /!resurrector!/) { |
| my ($canid, $canpath); |
| my ($canid, $canpath); |
| |
| |
| $canid = $1 if $hdr{__BODY__} =~ |
| $canid = $1 if $hdr{__BODY__} =~ |
| /\n========= WAS CANCELLED BY =======:.*\nMessage-ID: (.*?)\n/s; |
| /\n========= WAS CANCELLED BY =======:.*\nMessage-ID: (.*?)\n/s; |
| return reject('Redundant REPOST (cache)') |
| return reject('Redundant REPOST (cache)') |
| if $canid and $MIDhistory->check($canid); |
| if $canid and $MIDhistory->check($canid); |
| return reject('Redundant REPOST (ID)') |
| return reject('Redundant REPOST (ID)') |
| if $canid =~ /^<(?:[a-z]{16,17}|[0-9]{10}|[0-9]{10})\@/ |
| if $canid =~ /^<(?:[a-z]{16,17}|[0-9]{10}|[0-9]{10})\@/ |
| or $canid =~ /^<(?:cancel\.)*[0-9A-F]{8,15}\.[a-z]{4,11}\@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com)>$/; |
| or $canid =~ /^<(?:cancel\.)*[0-9A-F]{8,15}\.[a-z]{4,11}\@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com)>$/; |
| } |
| } |
| |
| |
| # basic checks on headers ############################################ |
| # basic checks on headers ############################################ |
| if ($gr{adult}) { |
| if ($gr{adult}) { |
| foreach (@Path_Entries) { |
| foreach (@Path_Entries) { |
| return reject("Bad path ($_)", 'Bad site') |
| return reject("Bad path ($_)", 'Bad site') |
| if exists $Bad_Adult_Path{$_}; |
| if exists $Bad_Adult_Path{$_}; |
| } |
| } |
| } |
| } |
| # If an article is fed locally and contains an Approved header, reject |
| # If an article is fed locally and contains an Approved header, reject |
| # it unless all groups match config{local_approved_groups}. |
| # it unless all groups match config{local_approved_groups}. |
| if ($state{localfeed} and $hdr{Approved}) { |
| if ($state{localfeed} and $hdr{Approved}) { |
| return reject("Forged approval: $hdr{Approved}", |
| return reject("Forged approval: $hdr{Approved}", |
| 'Forged Approval') |
| 'Forged Approval') |
| unless $gr{approved}; |
| unless $gr{approved}; |
| }; |
| }; |
| |
| |
| # Do any elements of the Path appear more than once. |
| # Do any elements of the Path appear more than once. |
| # TODO Wow, this filter catches a lot. Too much! Need to make it a |
| # TODO Wow, this filter catches a lot. Too much! Need to make it a |
| # scoring element rather than a reject. |
| # scoring element rather than a reject. |
| #%seen = {}; |
| #%seen = {}; |
| #@Path_Entries2 = grep {!$seen{$_}++ } @Path_Entries; |
| #@Path_Entries2 = grep {!$seen{$_}++ } @Path_Entries; |
| #return reject('Duplicated Path entry', 'Bad Path') |
| #return reject('Duplicated Path entry', 'Bad Path') |
| # if (scalar @Path_Entries) != (scalar @Path_Entries2); |
| # if (scalar @Path_Entries) != (scalar @Path_Entries2); |
| |
| |
| return reject('U2 violation - invalid distribution', 'U2 violation') |
| return reject('U2 violation - invalid distribution', 'U2 violation') |
| if $gr{rg_net} and $hdr{Distribution} !~ /^[ \t]*4[Gg][Hh][ \t]*$/; |
| if $gr{rg_net} and $hdr{Distribution} !~ /^[ \t]*4[Gg][Hh][ \t]*$/; |
| |
| |
| return reject('U2 violation - excessive crossposting', 'U2 violation') |
| return reject('U2 violation - excessive crossposting', 'U2 violation') |
| if $gr{rg_net} and $state{fupcnt} > 3; |
| if $gr{rg_net} and $state{fupcnt} > 3; |
| |
| |
| return reject('bofh violation - excessive crossposting','U2 violation') |
| return reject('bofh violation - excessive crossposting','U2 violation') |
| if $gr{rg_bofh} and $state{fupcnt} > 3; |
| if $gr{rg_bofh} and $state{fupcnt} > 3; |
| |
| |
| return reject('bofh violation - invalid distribution', 'U2 violation') |
| return reject('bofh violation - invalid distribution', 'U2 violation') |
| if $gr{rg_bofh} |
| if $gr{rg_bofh} |
| and $hdr{Distribution} !~ /^[ \t]*[Bb][Oo][Ff][Hh][ \t]*$/; |
| and $hdr{Distribution} !~ /^[ \t]*[Bb][Oo][Ff][Hh][ \t]*$/; |
| |
| |
| return reject('Too many newsgroups') |
| return reject('Too many newsgroups') |
| if $state{fupcnt} > $config{maxgroups}; |
| if $state{fupcnt} > $config{maxgroups}; |
| |
| |
| return reject('Too many newsgroups (low_xpost)', 'Too many newsgroups') |
| return reject('Too many newsgroups (low_xpost)', 'Too many newsgroups') |
| if $gr{low_xpost} |
| if $gr{low_xpost} |
| and $state{fupcnt} > $config{low_xpost_maxgroups}; |
| and $state{fupcnt} > $config{low_xpost_maxgroups}; |
| |
| |
| return reject('Too many newsgroups (meow)', 'Too many newsgroups') |
| return reject('Too many newsgroups (meow)', 'Too many newsgroups') |
| if $gr{meow} |
| if $gr{meow} |
| #and $gr{meow} != scalar @groups |
| #and $gr{meow} != scalar @groups |
| and $config{meow_ext_maxgroups} |
| and $config{meow_ext_maxgroups} |
| and ($state{fupcnt} - $gr{meow}) > $config{meow_ext_maxgroups}; |
| and ($state{fupcnt} - $gr{meow}) > $config{meow_ext_maxgroups}; |
| |
| |
| return reject('Topic Filter (Off-Topic1)', 'Topic Filter') |
| return reject('Topic Filter (Off-Topic1)', 'Topic Filter') |
| if $gr{topic1} |
| if $gr{topic1} |
| and $gr{topic1} >= $config{on_topic1_mingroups} |
| and $gr{topic1} >= $config{on_topic1_mingroups} |
| and $config{off_topic1_maxgroups} |
| and $config{off_topic1_maxgroups} |
| and $state{fupcnt} - $gr{topic1} > $config{off_topic1_maxgroups}; |
| and $state{fupcnt} - $gr{topic1} > $config{off_topic1_maxgroups}; |
| |
| |
| return reject('Topic Filter (On-Topic1)', 'Topic Filter') |
| return reject('Topic Filter (On-Topic1)', 'Topic Filter') |
| if $gr{topic1} and $config{on_topic1_maxgroups} |
| if $gr{topic1} and $config{on_topic1_maxgroups} |
| and $gr{topic1} > $config{on_topic1_maxgroups}; |
| and $gr{topic1} > $config{on_topic1_maxgroups}; |
| |
| |
| return reject('Topic Filter (topic2)', 'Topic Filter') |
| return reject('Topic Filter (topic2)', 'Topic Filter') |
| if $gr{topic2} |
| if $gr{topic2} |
| and $gr{topic2} >= $config{on_topic2_mingroups} |
| and $gr{topic2} >= $config{on_topic2_mingroups} |
| and $config{off_topic2_maxgroups} |
| and $config{off_topic2_maxgroups} |
| and $state{fupcnt} - $gr{topic2} > $config{off_topic2_maxgroups}; |
| and $state{fupcnt} - $gr{topic2} > $config{off_topic2_maxgroups}; |
| |
| |
| return reject('Topic Filter (On-Topic2)', 'Topic Filter') |
| return reject('Topic Filter (On-Topic2)', 'Topic Filter') |
| if $gr{topic2} and $config{on_topic2_maxgroups} |
| if $gr{topic2} and $config{on_topic2_maxgroups} |
| and $gr{topic2} > $config{on_topic2_maxgroups}; |
| and $gr{topic2} > $config{on_topic2_maxgroups}; |
| |
| |
| return reject('Too many test groups in crosspost', |
| return reject('Too many test groups in crosspost', |
| 'Too many newsgroups') if $gr{test} > 2; |
| 'Too many newsgroups') if $gr{test} > 2; |
| |
| |
| return reject('Excessively crossposted test article', |
| return reject('Excessively crossposted test article', |
| 'Too many newsgroups') if $gr{test} and $state{fupcnt} > 4; |
| 'Too many newsgroups') if $gr{test} and $state{fupcnt} > 4; |
| |
| |
| return reject('Adult group ECP', 'Too many newsgroups') |
| return reject('Adult group ECP', 'Too many newsgroups') |
| if $state{fupcnt} > 6 and $gr{adult} > $state{grpcnt} / 2; |
| if $state{fupcnt} > 6 and $gr{adult} > $state{grpcnt} / 2; |
| |
| |
| return reject('Poison newsgroup') if $gr{poison} and $state{grpcnt} > 1; |
| return reject('Poison newsgroup') if $gr{poison} and $state{grpcnt} > 1; |
| |
| |
| foreach (@Restricted_List) { |
| foreach (@Restricted_List) { |
| return reject("hierarchy violation - crosspost outside $_") |
| return reject("hierarchy violation - crosspost outside $_") |
| if $gr{'rg_'.$_} and not $gr{'rg_'.$_.'_only'}; |
| if $gr{'rg_'.$_} and not $gr{'rg_'.$_.'_only'}; |
| } |
| } |
| |
| |
| # binaries and MIME checks ########################################### |
| # binaries and MIME checks ########################################### |
| # XXX this protects the binary filters, but should not be needed anymore |
| # XXX this protects the binary filters, but should not be needed anymore |
| # with (?>...). If your server seems to hang try uncommenting this |
| # with (?>...). If your server seems to hang try uncommenting this |
| # killer article? |
| # killer article? |
| # return '' if $lines > 8000 and length $hdr{__BODY__} < $lines * 4; |
| # return '' if $lines > 8000 and length $hdr{__BODY__} < $lines * 4; |
| |
| |
| # short uuencoded html, text, exe, url files |
| # short uuencoded html, text, exe, url files |
| return reject("UUencoded $1") |
| return reject("UUencoded $1") |
| if $state{lines} > 3 and $state{lines} < 2000 |
| if $state{lines} > 3 and $state{lines} < 2000 |
| and $hdr{__BODY__} =~ / |
| and $hdr{__BODY__} =~ / |
| ^[Bb][Ee][Gg][Ii][Nn]$hws+[0-7]{3,4}$hws+ # begin 666 |
| ^[Bb][Ee][Gg][Ii][Nn]$hws+[0-7]{3,4}$hws+ # begin 666 |
| \S?.{0,45}?\S* # file name |
| \S?.{0,45}?\S* # file name |
| \.( # file extensions |
| \.( # file extensions |
| [Tt][Ee]?[Xx][Tt]| |
| [Tt][Ee]?[Xx][Tt]| |
| [Hh][Tt][Mm][Ll]?| |
| [Hh][Tt][Mm][Ll]?| |
| [Ee][Xx][Ee]| |
| [Ee][Xx][Ee]| |
| [Uu][Rr][Ll] |
| [Uu][Rr][Ll] |
| ) |
| ) |
| $hws+ # end of line |
| $hws+ # end of line |
| (?: |
| (?: |
| ^[ \t|>]* # skip quoting marks, if any |
| ^[ \t|>]* # skip quoting marks, if any |
| (?> # disable backtracking |
| (?> # disable backtracking |
| M[\x20-\x60]{60,61} # uuencoded line |
| M[\x20-\x60]{60,61} # uuencoded line |
| ) |
| ) |
| $hws*\n # trailing spaces and end of line |
| $hws*\n # trailing spaces and end of line |
| ){2,}? # 0 or > 2 lines |
| ){2,}? # 0 or > 2 lines |
| /mx; |
| /mx; |
| |
| |
| # binaries in non-binary newsgroups |
| # binaries in non-binary newsgroups |
| if ($config{block_binaries} or $config{block_all_binaries}) { |
| if ($config{block_binaries} or $config{block_all_binaries}) { |
| unless ($config{binaries_in_mod_groups} and $gr{allmod}) { |
| unless ($config{binaries_in_mod_groups} and $gr{allmod}) { |
| # We're only interested in binaries |
| # We're only interested in binaries |
| if (is_binary()) { |
| if (is_binary()) { |
| # Is the binary an image? |
| # Is the binary an image? |
| if ($config{image_extensions} |
| if ($config{image_extensions} |
| |
| and defined $state{file_extension}
|
| and $state{file_extension} =~ /$config{image_extensions}/) { |
| and $state{file_extension} =~ /$config{image_extensions}/) { |
| return reject("Binary Image: misplaced $state{file_extension}") |
| return reject("Binary Image: misplaced $state{file_extension}") |
| if not $gr{image}; |
| if not $gr{image}; |
| # gr{image} is true when distro matches bin_allowed |
| # gr{image} is true when distro matches bin_allowed |
| # or image_allowed |
| # or image_allowed |
| # gr{binary} is true when distro matches bin_allowed |
| # gr{binary} is true when distro matches bin_allowed |
| } else { |
| } else { |
| if (not $gr{binary}) { |
| if (not $gr{binary}) { |
| return reject("Binary: misplaced binary"); |
| return reject("Binary: misplaced binary"); |
| }; |
| }; |
| }; # End of misplaced binary check |
| }; # End of misplaced binary check |
| # If configured to reject all binary, we do it last so |
| # If configured to reject all binary, we do it last so |
| # misplaced stuff is still highlighted. |
| # misplaced stuff is still highlighted. |
| return reject("Binary Payload") |
| return reject("Binary Payload") |
| if $config{block_all_binaries}; |
| if $config{block_all_binaries}; |
| }; # End of is_binary |
| }; # End of is_binary |
| }; # End of moderated groups |
| }; # End of moderated groups |
| }; # End of block binaries |
| }; # End of block binaries |
| |
| |
| if ($config{block_mime_html} and not $gr{mime_html}) { |
| if ($config{block_mime_html} and not $gr{mime_html}) { |
| # MIME encapsulated HTML (attached *.html file) |
| # MIME encapsulated HTML (attached *.html file) |
| return reject('HTML file attachment', 'HTML') |
| return reject('HTML file attachment', 'HTML') |
| if $lch{'content-type'} =~ /multipart/ |
| if $lch{'content-type'} =~ /multipart/ |
| and ($hdr{__BODY__} =~ /^Content-Disposition:.*filename.*\.html?/imo |
| and ($hdr{__BODY__} =~ /^Content-Disposition:.*filename.*\.html?/imo |
| or $hdr{__BODY__} =~ /^Content-Base:.*file:.*\.html?/imo); |
| or $hdr{__BODY__} =~ /^Content-Base:.*file:.*\.html?/imo); |
| |
| |
| # Only one of the following two subsections can be applied, |
| # Only one of the following two subsections can be applied, |
| # depending on the state of config{block_html_multipart}. If it's |
| # depending on the state of config{block_html_multipart}. If it's |
| # True then all multipart mime with html elements will be |
| # True then all multipart mime with html elements will be |
| # rejected. If it's false, only multipart *without* text/plain |
| # rejected. If it's false, only multipart *without* text/plain |
| # elements will be rejected. |
| # elements will be rejected. |
| |
| |
| # MIME text/html without text/plain |
| # MIME text/html without text/plain |
| return reject('HTML Multipart without Text/Plain.', 'HTML') |
| return reject('HTML Multipart without Text/Plain.', 'HTML') |
| if not $config{block_html_multipart} |
| if not $config{block_html_multipart} |
| and $lch{'content-type'} =~ /multipart/ |
| and $lch{'content-type'} =~ /multipart/ |
| and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo |
| and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo |
| and not $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/plain#imo; |
| and not $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/plain#imo; |
| |
| |
| # MIME HTML regardless of text/plain element |
| # MIME HTML regardless of text/plain element |
| return reject('HTML Multipart', 'HTML') |
| return reject('HTML Multipart', 'HTML') |
| if $config{block_html_multipart} |
| if $config{block_html_multipart} |
| and $lch{'content-type'} =~ /multipart/ |
| and $lch{'content-type'} =~ /multipart/ |
| and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo; |
| and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo; |
| }; |
| }; |
| |
| |
| # HTML Inline |
| # HTML Inline |
| if ($config{block_html} and not $gr{html}) { |
| if ($config{block_html} and not $gr{html}) { |
| # HTML formatted postings |
| # HTML formatted postings |
| return reject('HTML post', 'HTML') |
| return reject('HTML post', 'HTML') |
| if $lch{'content-type'} =~ m#text/html#; |
| if $lch{'content-type'} =~ m#text/html#; |
| }; |
| }; |
| |
| |
| # Some consider <img> tags as a greater evil than other HTML. |
| # Some consider <img> tags as a greater evil than other HTML. |
| if ($config{block_html_images} |
| if ($config{block_html_images} |
| and $lch{'content-type'} !~ m#text/plain#) { |
| and $lch{'content-type'} !~ m#text/plain#) { |
| return reject('HTML Image Tags', 'HTML') |
| return reject('HTML Image Tags', 'HTML') |
| if $hdr{__BODY__} =~ /\<img$hws+src/i; |
| if $hdr{__BODY__} =~ /\<img$hws+src/i; |
| }; |
| }; |
| |
| |
| |
| |
| |
| |
| # bot checks ######################################################### |
| # bot checks ######################################################### |
| return reject('MID-Bot', 'Bot signature') |
| return reject('MID-Bot', 'Bot signature') |
| if $lch{'message-id'} =~ |
| if $lch{'message-id'} =~ |
| /(?: |
| /(?: |
| ^<\d{12}\@[a-z]{10}>$| |
| ^<\d{12}\@[a-z]{10}>$| |
| \@\d+>$| |
| \@\d+>$| |
| msgidabcxyz\.com>$| |
| msgidabcxyz\.com>$| |
| no(?:ne|where)\d+\.yet>$| |
| no(?:ne|where)\d+\.yet>$| |
| strip_path>$| |
| strip_path>$| |
| ^<[^ \t\.]+\@\d+G\d+O\d+O\d+F\d+.com>$ |
| ^<[^ \t\.]+\@\d+G\d+O\d+O\d+F\d+.com>$ |
| )/x; |
| )/x; |
| |
| |
| if ($hdr{'User-Agent'}) { |
| if ($hdr{'User-Agent'}) { |
| } elsif ($hdr{'X-Mailer'}) { |
| } elsif ($hdr{'X-Mailer'}) { |
| return reject('Message-ID/X-Mailer bot', 'Bot signature') |
| return reject('Message-ID/X-Mailer bot', 'Bot signature') |
| if $hdr{'Message-ID'} =~ /^<(.*)@/ |
| if $hdr{'Message-ID'} =~ /^<(.*)@/ |
| and $hdr{'X-Mailer'} eq $1; |
| and $hdr{'X-Mailer'} eq $1; |
| } elsif ($hdr{'X-Newsreader'}) { |
| } elsif ($hdr{'X-Newsreader'}) { |
| return reject('Smart Post Pro', 'Bot signature') |
| return reject('Smart Post Pro', 'Bot signature') |
| if $hdr{'X-Newsreader'} =~ /^[a-z]{7,11}$/ |
| if $hdr{'X-Newsreader'} =~ /^[a-z]{7,11}$/ |
| and $hdr{From} =~ /^[a-z]{7,13}\@[a-z]{7,12}\.com$/; |
| and $hdr{From} =~ /^[a-z]{7,13}\@[a-z]{7,12}\.com$/; |
| } else { |
| } else { |
| my $pathtail = ''; |
| my $pathtail = ''; |
| my $fromhost = ''; |
| my $fromhost = ''; |
| $hdr{Path} =~ /.*!(.*)$/ and $pathtail = $1; |
| $hdr{Path} =~ /.*!(.*)$/ and $pathtail = $1; |
| $hdr{From} =~ /@(.*?)>?$/ and $fromhost = $1; |
| $hdr{From} =~ /@(.*?)>?$/ and $fromhost = $1; |
| |
| |
| # Path/Newsgroups bot, contains just one MIME part |
| # Path/Newsgroups bot, contains just one MIME part |
| return reject('PN bot', 'Bot signature') |
| return reject('PN bot', 'Bot signature') |
| if $pathtail eq $hdr{Newsgroups} |
| if $pathtail eq $hdr{Newsgroups} |
| and $hdr{From} !~ /\Q$pathtail\E\@/ |
| and $hdr{From} !~ /\Q$pathtail\E\@/ |
| and $hdr{'Content-Type'} |
| and $hdr{'Content-Type'} |
| =~ /^multipart; boundary="_NextPart_/; |
| =~ /^multipart; boundary="_NextPart_/; |
| |
| |
| # Path/From/Message-ID bot |
| # Path/From/Message-ID bot |
| if ($hdr{'Message-ID'} =~ /^<\d{8}\.?\d{4}\@\Q$fromhost\E>$/) { |
| if ($hdr{'Message-ID'} =~ /^<\d{8}\.?\d{4}\@\Q$fromhost\E>$/) { |
| return reject('PFM bot path') if $pathtail eq $fromhost; |
| return reject('PFM bot path') if $pathtail eq $fromhost; |
| return reject('PFM bot misc', 'Bot signature') |
| return reject('PFM bot misc', 'Bot signature') |
| if $hdr{Subject} !~ / \d+ bytes \(\d+\/\d+\)$/; |
| if $hdr{Subject} !~ / \d+ bytes \(\d+\/\d+\)$/; |
| } |
| } |
| } # no X-Mailer/X-Newsreader/User-Agent header |
| } # no X-Mailer/X-Newsreader/User-Agent header |
| |
| |
| $state{xreader} = x_reader(); |
| $state{xreader} = x_reader(); |
| return reject("X-Bot ($state{xreader})", 'Bot signature') |
| return reject("X-Bot ($state{xreader})", 'Bot signature') |
| if $state{xreader} =~ /$Xbot/; |
| if $state{xreader} =~ /$Xbot/; |
| |
| |
| return reject('Email Platinum', 'Bot signature') |
| return reject('Email Platinum', 'Bot signature') |
| if $lch{organization} =~ /email platinum/; |
| if $lch{organization} =~ /email platinum/; |
| |
| |
| if (not $gr{reports} and not $hdr{References}) { |
| if (not $gr{reports} and not $hdr{References}) { |
| return reject('Bot - Newsgroup autoposter', 'Bot signature') |
| return reject('Bot - Newsgroup autoposter', 'Bot signature') |
| if $hdr{__BODY__} |
| if $hdr{__BODY__} |
| =~ /\n---[\r\n]+[A-Z][a-z \t]{120,}\.?[\r\n]+/; |
| =~ /\n---[\r\n]+[A-Z][a-z \t]{120,}\.?[\r\n]+/; |
| return reject('Angle-bracket bot', 'Bot signature') |
| return reject('Angle-bracket bot', 'Bot signature') |
| if $hdr{__BODY__} =~ /[\r<=>]+\r[\r<=>]+$/m; |
| if $hdr{__BODY__} =~ /[\r<=>]+\r[\r<=>]+$/m; |
| } |
| } |
| |
| |
| if (defined &local_filter_bot) { |
| if (defined &local_filter_bot) { |
| my @result = local_filter_bot(); |
| my @result = local_filter_bot(); |
| return reject(@result) if $result[0]; |
| return reject(@result) if $result[0]; |
| } |
| } |
| |
| |
| # EMP checks ######################################################### |
| # EMP checks ######################################################### |
| # create MD5 body checksum hash. |
| # create MD5 body checksum hash. |
| if ($config{do_md5} and not $gr{md5skip} and not $gr{alltest} |
| if ($config{do_md5} and not $gr{md5skip} and not $gr{alltest} |
| and not ($hdr{References} and $config{md5_skips_followups}) |
| and not ($hdr{References} and $config{md5_skips_followups}) |
| and (($config{md5_max_length} |
| and (($config{md5_max_length} |
| and $state{lines} < $config{md5_max_length}) |
| and $state{lines} < $config{md5_max_length}) |
| or not $config{md5_max_length}) |
| or not $config{md5_max_length}) |
| and $state{lines} > 0 and ($state{lines} > 2 |
| and $state{lines} > 0 and ($state{lines} > 2 |
| or ($state{lines} < 3 |
| or ($state{lines} < 3 |
| and $hdr{__BODY__} !~ /^\s{0,8}$/))) { |
| and $hdr{__BODY__} !~ /^\s{0,8}$/))) { |
| my $mbody; |
| my $mbody; |
| if ($config{fuzzy_md5} |
| if ($config{fuzzy_md5} |
| and (($config{fuzzy_max_length} |
| and (($config{fuzzy_max_length} |
| and $state{lines} < $config{fuzzy_max_length}) |
| and $state{lines} < $config{fuzzy_max_length}) |
| or not $config{fuzzy_max_length}) |
| or not $config{fuzzy_max_length}) |
| and not is_binary()) { |
| and not is_binary()) { |
| $mbody = lc $hdr{__BODY__}; |
| $mbody = lc $hdr{__BODY__}; |
| $mbody =~ s/^(?!http)\S{7,70}\r?$//mg; |
| $mbody =~ s/^(?!http)\S{7,70}\r?$//mg; |
| $mbody =~ s/^>+\s.*//mg; # Strip quoted lines |
| $mbody =~ s/^>+\s.*//mg; # Strip quoted lines |
| $mbody =~ s/\r{3}.*$//mg; |
| $mbody =~ s/\r{3}.*$//mg; |
| $mbody =~ s/$hws+$//; |
| $mbody =~ s/$hws+$//; |
| $mbody =~ s/^[^\n]*\Z//m if $state{lines} > 5; |
| $mbody =~ s/^[^\n]*\Z//m if $state{lines} > 5; |
| $mbody =~ tr/a-z0-9//cd; |
| $mbody =~ tr/a-z0-9//cd; |
| } |
| } |
| return reject('EMP (md5)', 'EMP') |
| return reject('EMP (md5)', 'EMP') |
| if $MD5history->add(md5_hex($mbody || $hdr{__BODY__})); |
| if $MD5history->add(md5_hex($mbody || $hdr{__BODY__})); |
| } |
| } |
| |
| |
| if (not $gr{reports}) { |
| if (not $gr{reports}) { |
| # create posting-host/lines hash |
| # create posting-host/lines hash |
| if ($config{do_phl} and not $gr{allmod} |
| if ($config{do_phl} and not $gr{allmod} |
| and $state{posting_host} and not $gr{phlskip} |
| and $state{posting_host} and not $gr{phlskip} |
| and not is_binary() and not $gr{alltest} |
| and not is_binary() and not $gr{alltest} |
| and not $state{posting_host} =~ /(?:$config{phl_exempt})/o |
| and not $state{posting_host} =~ /(?:$config{phl_exempt})/o |
| and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o |
| and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o |
| and not ($gr{binary} and $state{lines} > 100 |
| and not ($gr{binary} and $state{lines} > 100 |
| and $hdr{Subject} =~ /[\(\[]\d+\/\d+[\)\]]/)) { |
| and $hdr{Subject} =~ /[\(\[]\d+\/\d+[\)\]]/)) { |
| return reject('EMP (phl)', 'EMP') |
| return reject('EMP (phl)', 'EMP') |
| if $PHLhistory->add("$state{posting_host} $state{lines}"); |
| if $PHLhistory->add("$state{posting_host} $state{lines}"); |
| }; # End of PHL filter |
| }; # End of PHL filter |
| |
| |
| # create posting-host/newsgroups hash |
| # create posting-host/newsgroups hash |
| if ($config{do_phn} and not $gr{phrinc} and not $gr{phnskip} |
| if ($config{do_phn} and not $gr{phrinc} and not $gr{phnskip} |
| and not $gr{alltest} and not $gr{allmod} |
| and not $gr{alltest} and not $gr{allmod} |
| and not ($gr{binary} and $state{lines} > 100)) { |
| and not ($gr{binary} and $state{lines} > 100)) { |
| if ($state{posting_host} |
| if ($state{posting_host} |
| and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) { |
| and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) { |
| if (not $state{posting_host} =~ /(?:$config{phn_exempt})/o) { |
| if (not $state{posting_host} =~ /(?:$config{phn_exempt})/o) { |
| return reject('EMP (phn nph)', 'EMP') |
| return reject('EMP (phn nph)', 'EMP') |
| if $PHNhistory->add("$state{posting_host} $sortgrps"); |
| if $PHNhistory->add("$state{posting_host} $sortgrps"); |
| }; |
| }; |
| } elsif ($config{phn_aggressive}) { |
| } elsif ($config{phn_aggressive}) { |
| return reject('EMP (phn path)', 'EMP') |
| return reject('EMP (phn path)', 'EMP') |
| if $PHNhistory->add("$state{injection_host} $sortgrps"); |
| if $PHNhistory->add("$state{injection_host} $sortgrps"); |
| }; # End of aggressive mode |
| }; # End of aggressive mode |
| }; # End of PHN filter |
| }; # End of PHN filter |
| |
| |
| # create from/subject/lines hash |
| # create from/subject/lines hash |
| if ($config{do_fsl} and not $gr{fslskip} and not $gr{alltest}) { |
| if ($config{do_fsl} and not $gr{fslskip} and not $gr{alltest}) { |
| my $hash1; |
| my $hash1; |
| if (defined $hdr{Sender}) { |
| if (defined $hdr{Sender}) { |
| $hash1 = lc "$hdr{Sender} $hdr{Subject}"; |
| $hash1 = lc "$hdr{Sender} $hdr{Subject}"; |
| } else { |
| } else { |
| $hash1 = lc "$hdr{From} $hdr{Subject}"; |
| $hash1 = lc "$hdr{From} $hdr{Subject}"; |
| }; |
| }; |
| $hash1 =~ s/\d+$//; |
| $hash1 =~ s/\d+$//; |
| $hash1 =~ tr/a-z0-9\@\x80-\xFF//cd; |
| $hash1 =~ tr/a-z0-9\@\x80-\xFF//cd; |
| $hash1 = "$hash1 $state{lines}"; |
| $hash1 = "$hash1 $state{lines}"; |
| return reject('EMP (fsl)', 'EMP') if $FSLhistory->add($hash1); |
| return reject('EMP (fsl)', 'EMP') if $FSLhistory->add($hash1); |
| }; # End of FSL filter |
| }; # End of FSL filter |
| }; # not reports groups |
| }; # not reports groups |
| |
| |
| # create high-risk newsgroups hash |
| # create high-risk newsgroups hash |
| if ($config{do_phr} and $gr{phrinc} |
| if ($config{do_phr} and $gr{phrinc} |
| and not ($gr{binary} and $state{lines} > 100)) { |
| and not ($gr{binary} and $state{lines} > 100)) { |
| if ($state{posting_host} |
| if ($state{posting_host} |
| and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) { |
| and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) { |
| if (not $state{posting_host} =~ /(?:$config{phr_exempt})/o) { |
| if (not $state{posting_host} =~ /(?:$config{phr_exempt})/o) { |
| return reject('EMP (phr nph)', 'EMP') |
| return reject('EMP (phr nph)', 'EMP') |
| if $PHRhistory->add("$state{posting_host}"); |
| if $PHRhistory->add("$state{posting_host}"); |
| } |
| } |
| } elsif ($config{phr_aggressive}) { |
| } elsif ($config{phr_aggressive}) { |
| my $server; |
| my $server; |
| $server = lc "$hdr{Path}"; |
| $server = lc "$hdr{Path}"; |
| $server =~ s/(![^\.]+)+$//; # Strip right-most non-FQDN's |
| $server =~ s/(![^\.]+)+$//; # Strip right-most non-FQDN's |
| my $exc_count = ($server =~ tr/!//); # Count Path entries |
| my $exc_count = ($server =~ tr/!//); # Count Path entries |
| # We (probably) don't want to filter our immediate peers. |
| # We (probably) don't want to filter our immediate peers. |
| if ($exc_count > 1) { |
| if ($exc_count > 1) { |
| $server =~ s/.*!//; # Strip all but the right-most entry. |
| $server =~ s/.*!//; # Strip all but the right-most entry. |
| return reject('EMP (phr path)', 'EMP') |
| return reject('EMP (phr path)', 'EMP') |
| if $PHRhistory->add("$server"); |
| if $PHRhistory->add("$server"); |
| } |
| } |
| } |
| } |
| } # End of PHR filter |
| } # End of PHR filter |
| |
| |
| # Supersedes checks ################################################## |
| # Supersedes checks ################################################## |
| if ($hdr{Supersedes}) { |
| if ($hdr{Supersedes}) { |
| foreach (@Path_Entries) { |
| foreach (@Path_Entries) { |
| return reject("Supersedes with $_ in path", 'Rogue Supersedes') |
| return reject("Supersedes with $_ in path", 'Rogue Supersedes') |
| if exists $Bad_Cancel_Path{$_}; |
| if exists $Bad_Cancel_Path{$_}; |
| } |
| } |
| } |
| } |
| |
| |
| if ($config{do_supersedes_filter} and $hdr{Supersedes} |
| if ($config{do_supersedes_filter} and $hdr{Supersedes} |
| and not $state{posting_host} =~ /$config{supersedes_exempt}/o) { |
| and not $state{posting_host} =~ /$config{supersedes_exempt}/o) { |
| my $source; |
| my $source; |
| if ($state{posting_host}) { |
| if ($state{posting_host}) { |
| $source = lc $state{posting_host}; |
| $source = lc $state{posting_host}; |
| $source =~ tr/a-z.//cd; |
| $source =~ tr/a-z.//cd; |
| } |
| } |
| |
| |
| if ($source) { |
| if ($source) { |
| my $max; |
| my $max; |
| if ($gr{faq}) { $max = 45 } |
| if ($gr{faq}) { $max = $config{supersedes_max_faq} } |
| elsif (not ($config{active_file} or defined &INN::newsgroup)) |
| elsif (not ($config{active_file} or defined &INN::newsgroup)) |
| { $max = 10 } |
| { $max = $config{supersedes_max_unknown_active} } |
| elsif ($gr{allmod}) { $max = 35 } |
| elsif ($gr{allmod}) { $max = $config{supersedes_max_all_moderated} } |
| elsif ($gr{mod}) { $max = 10 } |
| elsif ($gr{mod}) { $max = $config{supersedes_max_moderated} } |
| else { $max = 6 } |
| else { $max = $config{supersedes_max_unmoderated} } |
| |
| |
| return reject('Excessive Supersedes ' |
| if ($config{supersedes_mode} ne 'off' |
| ."($state{posting_host})", 'Excessive Supersedes') |
| and $Suphistory->add2($source, $max)) { |
| if $Suphistory->add2($source, $max); |
| if ($config{supersedes_mode} eq 'audit') { |
| |
| $status{audited}++;
|
| |
| policy_count('supersedes', $state{peer}, $groups[0]);
|
| |
| policy_log_event('audit', 'supersedes',
|
| |
| "Excessive Supersedes ($state{posting_host})");
|
| |
| } else {
|
| |
| return reject('Excessive Supersedes '
|
| |
| ."($state{posting_host})", 'Excessive Supersedes');
|
| |
| }
|
| |
| }
|
| } |
| } |
| } |
| } |
| |
| |
| if (defined &local_filter_after_emp) { |
| if (defined &local_filter_after_emp) { |
| my @result = local_filter_after_emp(); |
| my @result = local_filter_after_emp(); |
| return reject(@result) if $result[0]; |
| return reject(@result) if $result[0]; |
| } |
| } |
| |
| |
| # bot checks, the second part ######################################## |
| # bot checks, the second part ######################################## |
| return reject('Fake multipart bot', 'Bot signature') |
| return reject('Fake multipart bot', 'Bot signature') |
| if $hdr{Subject} =~ m#\[(\d+)/(\d+)\]$# and $1 > $2; |
| if $hdr{Subject} =~ m#\[(\d+)/(\d+)\]$# and $1 > $2; |
| # |
| # |
| |
| |
| # Reject bad From headers. We also check Sender and Reply-To headers |
| # Reject bad From headers. We also check Sender and Reply-To headers |
| # against the same Regular Expression. |
| # against the same Regular Expression. |
| if ($Bad_From) { |
| if ($Bad_From) { |
| return reject("Banned Reply-To ($1)", 'Bad Reply-To') |
| return reject("Banned Reply-To ($1)", 'Bad Reply-To') |
| if $hdr{'Reply-To'} =~ $Bad_From_RE; |
| if $hdr{'Reply-To'} =~ $Bad_From_RE; |
| return reject("Banned Sender ($1)", 'Bad Sender') |
| return reject("Banned Sender ($1)", 'Bad Sender') |
| if $hdr{Sender} =~ $Bad_From_RE; |
| if $hdr{Sender} =~ $Bad_From_RE; |
| return reject("Banned From ($1)", 'Bad From') |
| return reject("Banned From ($1)", 'Bad From') |
| if $hdr{From} =~ $Bad_From_RE; |
| if $hdr{From} =~ $Bad_From_RE; |
| }; |
| }; |
| |
| |
| # Reject bad Subject headers. |
| # Reject bad Subject headers. |
| return reject("Subject ($1)", 'Bad Subject') |
| return reject("Subject ($1)", 'Bad Subject') |
| if $Bad_Subject and $hdr{Subject} =~ $Bad_Subject_RE; |
| if $Bad_Subject and $hdr{Subject} =~ $Bad_Subject_RE; |
| |
| |
| # Set a flag if the message body contains a URL. This means later, |
| # Set a flag if the message body contains a URL. This means later, |
| # more complex tests can be bypassed on many messages. |
| # more complex tests can be bypassed on many messages. |
| if ($body =~ /$HTTP/io) { |
| if ($body =~ /$HTTP/io) { |
| $state{body_has_url} = 1; |
| $state{body_has_url} = 1; |
| }; |
| }; |
| |
| |
| # Check payload against bad_body and bad_url files. |
| # Check payload against bad_body and bad_url files. |
| if (not $gr{reports} and not $hdr{References}) { |
| if (not $gr{reports} and not $hdr{References}) { |
| return reject("Body ($1)", 'Bad Body') |
| return reject("Body ($1)", 'Bad Body') |
| if $Bad_Body and $body =~ $Bad_Body_RE; |
| if $Bad_Body and $body =~ $Bad_Body_RE; |
| }; |
| }; |
| |
| |
| # Enforce Altopia policy on Path pre-loading. |
| # Enforce Altopia policy on Path pre-loading. |
| #if ($hdr{Path} =~ /news\.alt\.net!.*[!\.]alt\.net/) { |
| #if ($hdr{Path} =~ /news\.alt\.net!.*[!\.]alt\.net/) { |
| # return reject("TOS Violation") |
| # return reject("TOS Violation") |
| # if ($hdr{Path} !~ /[!\.]alt\.net(!not-for-mail)?$/); |
| # if ($hdr{Path} !~ /[!\.]alt\.net(!not-for-mail)?$/); |
| #}; |
| #}; |
| |
| |
| # bad words and scoring filter ####################################### |
| # bad words and scoring filter ####################################### |
| if ($config{do_scoring_filter} and not $gr{reports} |
| if ($config{do_scoring_filter} and not $gr{reports} |
| and not $gr{scoreskip}) { |
| and not $gr{scoreskip}) { |
| |
| |
| #FIXME All that's MIME isn't WebTV |
| #FIXME All that's MIME isn't WebTV |
| #$score .= "!!!webtv" if $lch{'content-type'} |
| #$score .= "!!!webtv" if $lch{'content-type'} |
| # =~ m#multipart/(?:related|mixed).*boundary# |
| # =~ m#multipart/(?:related|mixed).*boundary# |
| # and $hdr{'NNTP-Posting-Host'} !~ /webtv\.net$/ |
| # and $hdr{'NNTP-Posting-Host'} !~ /webtv\.net$/ |
| # and $lch{'message-id'} !~ /webtv\.net>$/; |
| # and $lch{'message-id'} !~ /webtv\.net>$/; |
| |
| |
| # Score the huge volume of commercial sex spam currently posted |
| # Score the huge volume of commercial sex spam currently posted |
| # to these groups. (20110817) |
| # to these groups. (20110817) |
| if ($state{spamsource}) { |
| if ($state{spamsource}) { |
| if ($hdr{Newsgroups} =~ /\.sex\.|^alt\.fan\.utb\./) { |
| if ($hdr{Newsgroups} =~ /\.sex\.|^alt\.fan\.utb\./) { |
| $score .= "!!!LongSexLine" if $hdr{__BODY__} =~ /^.{150}/m; |
| $score .= "!!!LongSexLine" if $hdr{__BODY__} =~ /^.{150}/m; |
| $score .= "!!SexLines1" if $state{lines} > 50; |
| $score .= "!!SexLines1" if $state{lines} > 50; |
| $score .= "!!SexLines2" if $state{lines} > 100; |
| $score .= "!!SexLines2" if $state{lines} > 100; |
| $score .= "!!!SexURL" if $state{body_has_url}; |
| $score .= "!!!SexURL" if $state{body_has_url}; |
| $score .= "!SexNoRef" if not $hdr{References}; |
| $score .= "!SexNoRef" if not $hdr{References}; |
| }; |
| }; |
| }; |
| }; |
| |
| |
| # Score poor netiquette on Follow-up's |
| # Score poor netiquette on Follow-up's |
| $score .= "!!ExpFup($state{grpcnt}-$state{grpfupcnt})" |
| $score .= "!!ExpFup($state{grpcnt}-$state{grpfupcnt})" |
| if $state{grpfupcnt} > $state{grpcnt}; |
| if $state{grpfupcnt} > $state{grpcnt}; |
| if (not $hdr{'Followup-To'}) { |
| if (not $hdr{'Followup-To'}) { |
| $score .= "!NoFup($state{grpcnt})" |
| $score .= "!NoFup($state{grpcnt})" |
| if $state{grpcnt} > 1 and $state{grpcnt} < 4; |
| if $state{grpcnt} > 1 and $state{grpcnt} < 4; |
| $score .= "!!NoFup($state{grpcnt})" if $state{grpcnt} >= 4; |
| $score .= "!!NoFup($state{grpcnt})" if $state{grpcnt} >= 4; |
| }; |
| }; |
| |
| |
| $score .= "!!!!FromURL2" if $lch{from} =~ /$url2/o; |
| $score .= "!!!!FromURL2" if $lch{from} =~ /$url2/o; |
| $score .= "!FromNoLC" if $lch{from} !~ /[a-z]/o; |
| $score .= "!FromNoLC" if $lch{from} !~ /[a-z]/o; |
| $score .= "!FromNo@" if $lch{from} !~ /\@/; |
| $score .= "!FromNo@" if $lch{from} !~ /\@/; |
| $score .= "!FromLen" if length($hdr{From}) > 80; |
| $score .= "!FromLen" if length($hdr{From}) > 80; |
| |
| |
| $score .= "!SubURL" if $lch{subject} =~ /$url/o; |
| $score .= "!SubURL" if $lch{subject} =~ /$url/o; |
| $score .= "!!!!!SubStealthURL" if $lch{subject} =~ /$stealthURL/o; |
| $score .= "!!!!!SubStealthURL" if $lch{subject} =~ /$stealthURL/o; |
| $score .= "!!Sub15Space" if $hdr{Subject} =~ / {15,}[^ ]/; |
| $score .= "!!Sub15Space" if $hdr{Subject} =~ / {15,}[^ ]/; |
| # Digits at the end are bad, but not if they are a year |
| # Digits at the end are bad, but not if they are a year |
| $score .= "!!!SubDigit" if $hdr{Subject} =~ /[\s~]\d{2,}$/ |
| $score .= "!!!SubDigit" if $hdr{Subject} =~ /[\s~]\d{2,}$/ |
| and $hdr{Subject} !~ /\D20[0-9]{2}$/; |
| and $hdr{Subject} !~ /\D20[0-9]{2}$/; |
| |
| |
| $score .= "!!!!SubDigitJPG" if $lch{subject} =~ /\s\d{1,3}\.jpg$/; |
| $score .= "!!!!SubDigitJPG" if $lch{subject} =~ /\s\d{1,3}\.jpg$/; |
| $score .= "!SubSymbols" if $hdr{Subject} =~ /^\W{3}/; |
| $score .= "!SubSymbols" if $hdr{Subject} =~ /^\W{3}/; |
| $score .= "!!!SubPhone" if $hdr{Subject} =~ /$phone/; |
| $score .= "!!!SubPhone" if $hdr{Subject} =~ /$phone/; |
| $score .= "!SubCR" if $hdr{Subject} =~ /\r/; |
| $score .= "!SubCR" if $hdr{Subject} =~ /\r/; |
| $score .= "!SubLength" if length($hdr{Subject}) > 210; |
| $score .= "!SubLength" if length($hdr{Subject}) > 210; |
| $score .= "!!SubNoLC" if $hdr{Subject} !~ /[a-z]/; |
| $score .= "!!SubNoLC" if $hdr{Subject} !~ /[a-z]/; |
| $score .= "!!!SubSpaced" if $hdr{Subject} =~ /(\w$hws+){8}/o; |
| $score .= "!!!SubSpaced" if $hdr{Subject} =~ /(\w$hws+){8}/o; |
| |
| |
| # Job Spam |
| # Job Spam |
| $score .= "!!!Jobs" if $hdr{Newsgroups} =~ /\.jobs|\.bestjobs/ |
| $score .= "!!!Jobs" if $hdr{Newsgroups} =~ /\.jobs|\.bestjobs/ |
| and $hdr{From} =~ /job/; |
| and $hdr{From} =~ /job/; |
| |
| |
| if ($config{aggressive} and not $gr{alladult}) { |
| if ($config{aggressive} and not $gr{alladult}) { |
| $score .= "!Sub1Pt($1)" while $lch{subject} =~ /($one_point_words)/go; |
| $score .= "!Sub1Pt($1)" while $lch{subject} =~ /($one_point_words)/go; |
| $score .= "!!Sub2Pt($1)" while $lch{subject} =~ /($two_point_words)/go; |
| $score .= "!!Sub2Pt($1)" while $lch{subject} =~ /($two_point_words)/go; |
| $score .= "!From1Pt" while $lch{from} =~ /$one_point_words/go; |
| $score .= "!From1Pt" while $lch{from} =~ /$one_point_words/go; |
| $score .= "!!From2Pt" while $lch{from} =~ /$two_point_words/go; |
| $score .= "!!From2Pt" while $lch{from} =~ /$two_point_words/go; |
| $score .= "!MID1Pt" while $lch{'message-id'} =~ /$one_point_words/go; |
| $score .= "!MID1Pt" while $lch{'message-id'} =~ /$one_point_words/go; |
| $score .= "!!MID2Pt" while $lch{'message-id'} =~ /$two_point_words/go; |
| $score .= "!!MID2Pt" while $lch{'message-id'} =~ /$two_point_words/go; |
| $score .= "!Org1Pt" while $lch{organization} =~ /$one_point_words/go; |
| $score .= "!Org1Pt" while $lch{organization} =~ /$one_point_words/go; |
| $score .= "!!Org2Pt" while $lch{organization} =~ /$two_point_words/go; |
| $score .= "!!Org2Pt" while $lch{organization} =~ /$two_point_words/go; |
| |
| |
| local $_ = $lch{subject}; |
| local $_ = $lch{subject}; |
| tr/a-z0-9 //cd; |
| tr/a-z0-9 //cd; |
| #FIXME Next two return false positives. |
| #FIXME Next two return false positives. |
| #$score .= "!!!!!SubServices" if /$services/o; |
| #$score .= "!!!!!SubServices" if /$services/o; |
| #$score .= "!!!SubSiteDesc" if /$site_desc.{0,20}site/o; |
| #$score .= "!!!SubSiteDesc" if /$site_desc.{0,20}site/o; |
| $score .= "!SubPorn" if /(?:$free_stuff|$porn)/o; |
| $score .= "!SubPorn" if /(?:$free_stuff|$porn)/o; |
| }; |
| }; |
| |
| |
| $score .= "!!SubImage" if $state{lines} < 30 |
| $score .= "!!SubImage" if $state{lines} < 30 |
| and $lch{subject}=~ /\w\.(?:jpe?g|gif)/; |
| and $lch{subject}=~ /\w\.(?:jpe?g|gif)/; |
| $score .= "!!MalfLines($state{badlines})" if $state{badlines} > 0; |
| $score .= "!!MalfLines($state{badlines})" if $state{badlines} > 0; |
| $score .= "!!!NoOrg" if $lch{organization} =~ /<no organization>/; |
| $score .= "!!!NoOrg" if $lch{organization} =~ /<no organization>/; |
| $score .= "!!!!!!!StealthOrg" if $lch{organization} =~ /$stealthURL/o; |
| $score .= "!!!!!!!StealthOrg" if $lch{organization} =~ /$stealthURL/o; |
| $score .= "!!!!!MIDDigit" if $hdr{'Message-ID'}=~/^<(?:\d{8}\.?\d{4}|\d{4,5})\@/; |
| $score .= "!!!!!MIDDigit" if $hdr{'Message-ID'}=~/^<(?:\d{8}\.?\d{4}|\d{4,5})\@/; |
| |
| |
| if ($lch{'content-type'} =~ m#^(?:multipart|text/html)#) { |
| if ($lch{'content-type'} =~ m#^(?:multipart|text/html)#) { |
| $score .= "***PGP" if $lch{'content-type'} =~ /^multipart\/signed/; |
| $score .= "***PGP" if $lch{'content-type'} =~ /^multipart\/signed/; |
| $score .= "!!!!!!!HTMLRefresh" if $body =~ /<meta http-equiv=.?refresh/; |
| $score .= "!!!!!!!HTMLRefresh" if $body =~ /<meta http-equiv=.?refresh/; |
| $score .= "!!!!!!!HTMLPopup" if $body =~ /window\.open\(/; |
| $score .= "!!!!!!!HTMLPopup" if $body =~ /window\.open\(/; |
| $score .= "!!!!!!HTMLJava" if $body =~ /<script language=.?javascript/; |
| $score .= "!!!!!!HTMLJava" if $body =~ /<script language=.?javascript/; |
| $score .= "!!!!!!HTMLLive" if $body =~ /<script language=.?livescript/; |
| $score .= "!!!!!!HTMLLive" if $body =~ /<script language=.?livescript/; |
| $score .= "!!HTMLAlternate" if $body =~ /^content-type:$hws+multipart\/alternative/m; |
| $score .= "!!HTMLAlternate" if $body =~ /^content-type:$hws+multipart\/alternative/m; |
| }; |
| }; |
| |
| |
| #$score .= "!CR" if $body =~ /\r/; |
| #$score .= "!CR" if $body =~ /\r/; |
| $score .= "!!!2CR" if $body =~ /\r\r/; |
| $score .= "!!!2CR" if $body =~ /\r\r/; |
| $score .= "!!Newlines" if $hdr{__BODY__} =~ /(\r?\n){15}/; |
| $score .= "!!Newlines" if $hdr{__BODY__} =~ /(\r?\n){15}/; |
| $score .= "!!Phone" if $body =~ /$phone/; |
| $score .= "!!Phone" if $body =~ /$phone/; |
| # Score rates per minute (1.99/m or 1.99 per min) |
| # Score rates per minute (1.99/m or 1.99 per min) |
| $score .= "!!!Rate" |
| $score .= "!!!Rate" |
| if $body =~ /\d\.\d{2}\s?(?:[\\\/]\s?m|$hws+per\bmin)/; |
| if $body =~ /\d\.\d{2}\s?(?:[\\\/]\s?m|$hws+per\bmin)/; |
| $score .= "!!!!!HttpIP" if $body =~ /$HTTP$IP/o; |
| $score .= "!!!!!HttpIP" if $body =~ /$HTTP$IP/o; |
| $score .= "!!!!!!!Stealth" if $body =~ /$HTTP$StealthIP/o; |
| $score .= "!!!!!!!Stealth" if $body =~ /$HTTP$StealthIP/o; |
| $score .= "!!!!SpaceCom" if $body =~ /$spacecom/o; |
| $score .= "!!!!SpaceCom" if $body =~ /$spacecom/o; |
| # Power Post generates lots of spam. |
| # Power Post generates lots of spam. |
| $score .= "!!!PowerPost" if $hdr{'X-Newsposter'} and not $gr{binary}; |
| $score .= "!!!PowerPost" if $hdr{'X-Newsposter'} and not $gr{binary}; |
| |
| |
| # only URL |
| # only URL |
| $score .= "!!!!!!OnlyURL" |
| $score .= "!!!!!!OnlyURL" |
| if $state{lines} < 3 and $body =~ /^$hws*$url3\S*$hws*$/o; |
| if $state{lines} < 3 and $body =~ /^$hws*$url3\S*$hws*$/o; |
| |
| |
| if ($state{body_has_url}) { |
| if ($state{body_has_url}) { |
| if ($Bad_URL) { |
| if ($Bad_URL) { |
| $score .= "!" x $config{'bad_url_score'} . 'bad_url' |
| $score .= "!" x $config{'bad_url_score'} . 'bad_url' |
| if $body =~ $Bad_URL_RE; |
| if $body =~ $Bad_URL_RE; |
| }; |
| }; |
| if ($Bad_URL_Central) { |
| if ($Bad_URL_Central) { |
| $score .= "!" x $config{'bad_url_score'} . 'bad_url_cen' |
| $score .= "!" x $config{'bad_url_score'} . 'bad_url_cen' |
| if $body =~ $Bad_URL_Central_RE; |
| if $body =~ $Bad_URL_Central_RE; |
| }; |
| }; |
| |
| |
| # Emphasized URL (>>> http://www.foo <<<) |
| # Emphasized URL (>>> http://www.foo <<<) |
| if (not $hdr{References}) { |
| if (not $hdr{References}) { |
| if ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL[\x09\x20]+$SYMBOL{3}/o) { |
| if ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL[\x09\x20]+$SYMBOL{3}/o) { |
| $score .= "!!!EmphURL3"; |
| $score .= "!!!EmphURL3"; |
| } elsif ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL/o) { |
| } elsif ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL/o) { |
| $score .= "!EmphURL1"; |
| $score .= "!EmphURL1"; |
| }; |
| }; |
| }; # End of References |
| }; # End of References |
| }; # End of body_has_url |
| }; # End of body_has_url |
| |
| |
| if ($hdr{References}) { |
| if ($hdr{References}) { |
| if ($hdr{References} =~ /^<[^>]+>\s*</) { |
| if ($hdr{References} =~ /^<[^>]+>\s*</) { |
| $score .= "***Refs"; |
| $score .= "***Refs"; |
| } elsif ($hdr{References} =~ /^<[^>]+>\s*$/) { |
| } elsif ($hdr{References} =~ /^<[^>]+>\s*$/) { |
| $score .= "**Ref"; |
| $score .= "**Ref"; |
| } else { |
| } else { |
| $score .= "!!BadRef"; |
| $score .= "!!BadRef"; |
| }; |
| }; |
| |
| |
| if ($state{lines} > 0) { |
| if ($state{lines} > 0) { |
| # How much of the post is quoted? |
| # How much of the post is quoted? |
| $state{reflines} = 0; |
| $state{reflines} = 0; |
| $state{reflines}++ while $hdr{__BODY__} =~ /^>/mg; |
| $state{reflines}++ while $hdr{__BODY__} =~ /^>/mg; |
| $state{pctrefs} = int(($state{reflines} / |
| $state{pctrefs} = int(($state{reflines} / |
| $state{lines}) * 100) / 100; |
| $state{lines}) * 100) / 100; |
| if ($state{pctrefs} > 0.9 and $state{lines} > 50) { |
| if ($state{pctrefs} > 0.9 and $state{lines} > 50) { |
| $score .= "!!PctQuote($state{pctrefs})"; |
| $score .= "!!PctQuote($state{pctrefs})"; |
| } elsif ($state{pctrefs} > 0.7 and $state{lines} > 30) { |
| } elsif ($state{pctrefs} > 0.7 and $state{lines} > 30) { |
| $score .= "!PctQuote($state{pctrefs})"; |
| $score .= "!PctQuote($state{pctrefs})"; |
| } elsif ($state{reflines} == 0 and $state{lines} > 5) { |
| } elsif ($state{reflines} == 0 and $state{lines} > 5) { |
| $score .= "!NoQuote"; |
| $score .= "!NoQuote"; |
| }; |
| }; |
| }; |
| }; |
| }; # End of References conditional |
| }; # End of References conditional |
| |
| |
| # Long signatures are poor netiquette but not spam, so only |
| # Long signatures are poor netiquette but not spam, so only |
| # give them low scores. |
| # give them low scores. |
| #$score .= "!SigLen" if $body =~ /\n-- (\r?\n.*){5,}\n\w/; |
| #$score .= "!SigLen" if $body =~ /\n-- (\r?\n.*){5,}\n\w/; |
| #$score .= "!SigLen2" if $body =~ /\n-- (\r?\n.*){9,}\n\w/; |
| #$score .= "!SigLen2" if $body =~ /\n-- (\r?\n.*){9,}\n\w/; |
| |
| |
| # Check messages for high ratios of upper case and urls. |
| # Check messages for high ratios of upper case and urls. |
| if ($config{do_ratio_scoring} and not $gr{ratio} |
| if ($config{do_ratio_scoring} and not $gr{ratio} |
| and $state{lines} < 4000 and not is_binary() |
| and $state{lines} < 4000 and not is_binary() |
| and not $hdr{'Content-Transfer-Encoding'} =~ /base64/ |
| and not $hdr{'Content-Transfer-Encoding'} =~ /base64/ |
| and not $state{charset} =~ /big5|iso-2022|koi8-r/) { |
| and not $state{charset} =~ /big5|iso-2022|koi8-r/) { |
| $fuzzy = $hdr{__BODY__}; |
| $fuzzy = $hdr{__BODY__}; |
| $state{urlcount} = $fuzzy =~ s/http:\/\/www\.\S+//g; # Strip urls |
| $state{urlcount} = $fuzzy =~ s/http:\/\/www\.\S+//g; # Strip urls |
| $state{urlcount} += $fuzzy =~ s/$HTTP\S+//g; # Strip urls |
| $state{urlcount} += $fuzzy =~ s/$HTTP\S+//g; # Strip urls |
| $state{nonasc} += $fuzzy =~ s/[\x80-\xFF]//g; # Strip non-ascii |
| $state{nonasc} += $fuzzy =~ s/[\x80-\xFF]//g; # Strip non-ascii |
| $fuzzy =~ s/[\-=]{5,}//g; # Strip horizontal rules |
| $fuzzy =~ s/[\-=]{5,}//g; # Strip horizontal rules |
| $fuzzy =~ s/[a-zA-Z]{20,}//g; # Strip long char strings |
| $fuzzy =~ s/[a-zA-Z]{20,}//g; # Strip long char strings |
| $state{letters} = $fuzzy =~ y/a-zA-Z//; # Count letters |
| $state{letters} = $fuzzy =~ y/a-zA-Z//; # Count letters |
| $state{uppercase} = $fuzzy =~ y/A-Z//; # Count upper |
| $state{uppercase} = $fuzzy =~ y/A-Z//; # Count upper |
| $state{symbols} = $fuzzy =~ y/!"#$%&'()*+,-:;=?@{|}~//; # Count syms |
| $state{symbols} = $fuzzy =~ y/!"#$%&'()*+,-:;=?@{|}~//; # Count syms |
| undef $fuzzy; |
| undef $fuzzy; |
| # Where non-ASCII chars are present, there should be a charset |
| # Where non-ASCII chars are present, there should be a charset |
| # specified in the Content-Type. |
| # specified in the Content-Type. |
| if (not $state{charset} and $state{nonasc}) { |
| if (not $state{charset} and $state{nonasc}) { |
| $score .= "!!!NoCharset" if $state{nonasc} > 50; |
| $score .= "!!!NoCharset" if $state{nonasc} > 50; |
| }; |
| }; |
| if (not $state{charset} =~ /iso-2022|koi8-r/) { |
| if (not $state{charset} =~ /iso-2022|koi8-r/) { |
| if ($state{uppercase} > 50) { |
| if ($state{uppercase} > 50) { |
| $state{capratio} = int(($state{uppercase} / |
| $state{capratio} = int(($state{uppercase} / |
| $state{letters}) * 100) / 100; |
| $state{letters}) * 100) / 100; |
| if ($state{capratio} > 0.8) { |
| if ($state{capratio} > 0.8) { |
| $score .= "!!!CapRat($state{capratio})"; |
| $score .= "!!!CapRat($state{capratio})"; |
| } elsif ($state{capratio} > 0.5) { |
| } elsif ($state{capratio} > 0.5) { |
| $score .= "!!CapRat($state{capratio})"; |
| $score .= "!!CapRat($state{capratio})"; |
| } elsif ($state{capratio} > 0.2) { |
| } elsif ($state{capratio} > 0.2) { |
| $score .= "!CapRat($state{capratio})"; |
| $score .= "!CapRat($state{capratio})"; |
| }; |
| }; |
| }; |
| }; |
| if ($state{letters} > 10 and not $hdr{References}) { |
| if ($state{letters} > 10 and not $hdr{References}) { |
| $state{symratio} = int(($state{symbols} / |
| $state{symratio} = int(($state{symbols} / |
| $state{letters}) * 100) / 100; |
| $state{letters}) * 100) / 100; |
| if ($state{symratio} > 1) { |
| if ($state{symratio} > 1) { |
| $score .= "!!!SymRat($state{symratio})"; |
| $score .= "!!!SymRat($state{symratio})"; |
| } elsif ($state{symratio} > 0.5) { |
| } elsif ($state{symratio} > 0.5) { |
| $score .= "!!SymRat($state{symratio})"; |
| $score .= "!!SymRat($state{symratio})"; |
| } elsif ($state{symratio} > 0.3) { |
| } elsif ($state{symratio} > 0.3) { |
| $score .= "!SymRat($state{symratio})"; |
| $score .= "!SymRat($state{symratio})"; |
| }; |
| }; |
| }; |
| }; |
| }; # End of charset conditional |
| }; # End of charset conditional |
| |
| |
| # Grant some slack to posts with no url's in them. |
| # Grant some slack to posts with no url's in them. |
| $score .= "**NoUrl" if $state{urlcount} == 0; |
| $score .= "**NoUrl" if $state{urlcount} == 0; |
| # Score on URL ratios |
| # Score on URL ratios |
| if ($state{lines} > 0 and $state{urlcount} > 1) { |
| if ($state{lines} > 0 and $state{urlcount} > 1) { |
| $urlscore = 0; |
| $urlscore = 0; |
| $state{urlratio} = int(($state{urlcount} / |
| $state{urlratio} = int(($state{urlcount} / |
| $state{lines}) * 100) / 100; |
| $state{lines}) * 100) / 100; |
| if ($state{urlratio} > 0.5 and $state{lines} > 40) { |
| if ($state{urlratio} > 0.5 and $state{lines} > 40) { |
| $urlscore = 3; |
| $urlscore = 3; |
| } elsif ($state{urlratio} > 0.3 and $state{lines} > 10) { |
| } elsif ($state{urlratio} > 0.3 and $state{lines} > 10) { |
| $urlscore = 2; |
| $urlscore = 2; |
| } elsif ($state{urlratio} > 0.1 and $state{lines} > 5) { |
| } elsif ($state{urlratio} > 0.1 and $state{lines} > 5) { |
| $urlscore = 1; |
| $urlscore = 1; |
| }; # End of url ratio calculation |
| }; # End of url ratio calculation |
| # Double the score for spamsources |
| # Double the score for spamsources |
| $urlscore = $urlscore * 2 if $state{spamsource}; |
| $urlscore = $urlscore * 2 if $state{spamsource}; |
| # Convert the score integer to text format |
| # Convert the score integer to text format |
| if ($urlscore > 0) { |
| if ($urlscore > 0) { |
| $score .= "!" x $urlscore . "UrlRat($state{urlratio})"; |
| $score .= "!" x $urlscore . "UrlRat($state{urlratio})"; |
| }; |
| }; |
| }; # End of url ratio scoring |
| }; # End of url ratio scoring |
| }; # End of do_ratio_scoring |
| }; # End of do_ratio_scoring |
| |
| |
| # Grant some slack to adult groups posts, providing they don't |
| # Grant some slack to adult groups posts, providing they don't |
| # originate from a spam source. |
| # originate from a spam source. |
| if ($gr{alladult} and not $state{spamsource}) { |
| if ($gr{alladult} and not $state{spamsource}) { |
| if ($hdr{References}) { |
| if ($hdr{References}) { |
| $score .= "***Adult"; |
| $score .= "***Adult"; |
| } else { |
| } else { |
| $score .= "**Adult" if not $state{body_has_url}; |
| $score .= "**Adult" if not $state{body_has_url}; |
| $score .= "#Adult+Url" if $state{body_has_url}; |
| $score .= "#Adult+Url" if $state{body_has_url}; |
| }; |
| }; |
| }; # End of Adult slack |
| }; # End of Adult slack |
| |
| |
| if ($config{active_file} or defined &INN::newsgroup) { |
| if ($config{active_file} or defined &INN::newsgroup) { |
| if ($gr{allmod}) { |
| if ($gr{allmod}) { |
| #$score -= 6; |
| #$score -= 6; |
| $score .= "******AllMod"; |
| $score .= "******AllMod"; |
| } elsif ($gr{mod}) { |
| } elsif ($gr{mod}) { |
| #$score -= 4; |
| #$score -= 4; |
| $score .= "****Mod"; |
| $score .= "****Mod"; |
| }; |
| }; |
| }; |
| }; |
| |
| |
| $state{scoreval} = ($score =~ tr/!//) - ($score =~ tr/*//); |
| $state{scoreval} = ($score =~ tr/!//) - ($score =~ tr/*//); |
| return reject("Scoring filter", "Scoring filter") |
| return reject("Scoring filter", "Scoring filter") |
| if $state{scoreval} > 7; |
| if $state{scoreval} > 7; |
| }; # End of Scoring filters |
| }; # End of Scoring filters |
| |
| |
| if (defined &local_filter_last) { |
| if (defined &local_filter_last) { |
| my @result = local_filter_last(); |
| my @result = local_filter_last(); |
| return reject(@result) if $result[0]; |
| return reject(@result) if $result[0]; |
| } |
| } |
| |
| |
| # cancel messages ######################################################## |
| # cancel messages ######################################################## |
| } elsif ($hdr{Control} =~ /^$hws*cancel/) { |
| } elsif ($hdr{Control} =~ /^$hws*cancel/) { |
| foreach (@Path_Entries) { |
| foreach (@Path_Entries) { |
| return reject("Cancel with $_ in path", 'Rogue cancel') |
| return reject("Cancel with $_ in path", 'Rogue cancel') |
| if exists $Bad_Cancel_Path{$_}; |
| if exists $Bad_Cancel_Path{$_}; |
| } |
| } |
| |
| |
| return reject('User-issued spam cancel') |
| return reject('User-issued spam cancel') |
| if $config{block_user_spamcancels} |
| if $config{block_user_spamcancels} |
| and $state{injection_host} and $state{posting_host} |
| and $state{injection_host} and $state{posting_host} |
| and $hdr{Path} =~ /!cyberspam!/; |
| and $hdr{Path} =~ /!cyberspam!/; |
| |
| |
| return reject('User-issued cancel') |
| return reject('User-issued cancel') |
| if $config{block_user_cancels} |
| if $config{block_user_cancels} |
| and not $hdr{Path} =~ /!cyberspam!/; |
| and not $hdr{Path} =~ /!cyberspam!/; |
| |
| |
| return reject('Cancel in forbidden group', 'Rogue cancel') |
| return reject('Cancel in forbidden group', 'Rogue cancel') |
| if $gr{no_cancel} and not $hdr{Path} =~ /!cyberspam!/; |
| if $gr{no_cancel} and not $hdr{Path} =~ /!cyberspam!/; |
| |
| |
| if ($config{block_late_cancels} |
| if ($config{block_late_cancels} |
| and $hdr{Control} =~ /^cancel$hws+(.+)$/) { |
| and $hdr{Control} =~ /^cancel$hws+(.+)$/) { |
| return reject('Cancel for rejected article') |
| return reject('Cancel for rejected article') |
| if $MIDhistory->check($1); |
| if $MIDhistory->check($1); |
| } |
| } |
| |
| |
| return reject('Cancel with Supersedes header') |
| return reject('Cancel with Supersedes header') |
| if $hdr{Supersedes}; |
| if $hdr{Supersedes}; |
| |
| |
| return reject('Rogue cancel (newsgroups)', 'Rogue cancel') |
| return reject('Rogue cancel (newsgroups)', 'Rogue cancel') |
| if grep(/^control(?:\.cancel)?$/, @groups); |
| if grep(/^control(?:\.cancel)?$/, @groups); |
| |
| |
| # from Ricardo's "FAQ" + hipcrime signatures |
| # from Ricardo's "FAQ" + hipcrime signatures |
| return reject("Rogue cancel ($1)", 'Rogue cancel') |
| return reject("Rogue cancel ($1)", 'Rogue cancel') |
| if $hdr{Path} =~ /(h[i\d]pcr[i\d]me|(?:hip|hacker|crack|porn|cripple|gimp|cunt|hole|fag|aids|faq|god|hindu|dothead|jew|kike|moslem|towelhead|nazi|kraut|nerd|geek|nigger|redneck|rice|slanteye|spick|whine)cancel|cyberwhin(?:er|ing))/; |
| if $hdr{Path} =~ /(h[i\d]pcr[i\d]me|(?:hip|hacker|crack|porn|cripple|gimp|cunt|hole|fag|aids|faq|god|hindu|dothead|jew|kike|moslem|towelhead|nazi|kraut|nerd|geek|nigger|redneck|rice|slanteye|spick|whine)cancel|cyberwhin(?:er|ing))/; |
| |
| |
| if ($hdr{'X-Cancelled-By'} or $hdr{'X-Canceled-By'}) { |
| if ($hdr{'X-Cancelled-By'} or $hdr{'X-Canceled-By'}) { |
| my $xcb = lc ($hdr{'X-Cancelled-By'} || $hdr{'X-Canceled-By'}); |
| my $xcb = lc ($hdr{'X-Cancelled-By'} || $hdr{'X-Canceled-By'}); |
| return reject('Bad X-Cancelled-By', 'Rogue cancel') |
| return reject('Bad X-Cancelled-By', 'Rogue cancel') |
| if $xcb !~ /\w\@\w/; |
| if $xcb !~ /\w\@\w/; |
| } |
| } |
| |
| |
| if (defined &local_filter_cancel) { |
| if (defined &local_filter_cancel) { |
| my @result = local_filter_cancel(); |
| my @result = local_filter_cancel(); |
| return reject(@result) if $result[0]; |
| return reject(@result) if $result[0]; |
| } |
| } |
| |
| |
| # newgroup and rmgroup messages ########################################## |
| # newgroup and rmgroup messages ########################################## |
| } elsif ($hdr{Control} =~ /^$hws*((?:new|rm)group)$hws+(.*)/) { |
| } elsif ($hdr{Control} =~ /^$hws*((?:new|rm)group)$hws+(.*)/) { |
| my $control_type = $1; |
| my $control_type = $1; |
| my $control_group = $2; |
| my $control_group = $2; |
| |
| |
| return reject("Bogus $control_type message from Collabra luser", |
| return reject("Bogus $control_type message from Collabra luser", |
| 'Bad control message') |
| 'Bad control message') |
| if $hdr{Distribution} =~ /collabra-internal/ or $hdr{__BODY__} |
| if $hdr{Distribution} =~ /collabra-internal/ or $hdr{__BODY__} |
| =~ /Control message generated by Netscape Collabra Server/; |
| =~ /Control message generated by Netscape Collabra Server/; |
| |
| |
| if ($control_group |
| if ($control_group |
| =~ /^(?:comp|misc|news|rec|soc|sci|humanities|talk)\./) { |
| =~ /^(?:comp|misc|news|rec|soc|sci|humanities|talk)\./) { |
| return reject("Big 8 $control_type message from wrong address", |
| return reject("Big 8 $control_type message from wrong address", |
| 'Bad control message') |
| 'Bad control message') |
| if $hdr{From} !~ /group-admin\@isc\.org/; |
| if $hdr{From} !~ /group-admin\@isc\.org/; |
| } else { |
| } else { |
| return reject("Forged non-big-8 $control_type message supposedly from tale", 'Bad control message') |
| return reject("Forged non-big-8 $control_type message supposedly from tale", 'Bad control message') |
| if $hdr{From} |
| if $hdr{From} |
| =~ /(?:group-admin|tale)\@isc\.org|tale\@uunet\.uu\.net/; |
| =~ /(?:group-admin|tale)\@isc\.org|tale\@uunet\.uu\.net/; |
| } |
| } |
| |
| |
| return reject("Unapproved $control_type message", |
| return reject("Unapproved $control_type message", |
| 'Bad control message') if not $hdr{Approved}; |
| 'Bad control message') if not $hdr{Approved}; |
| |
| |
| return reject("Newgroup for poison group $control_group", |
| return reject("Newgroup for poison group $control_group", |
| 'Bad control message') |
| 'Bad control message') |
| if $control_type eq 'newgroup' |
| if $control_type eq 'newgroup' |
| and $control_group =~ /$config{poison_groups}/o; |
| and $control_group =~ /$config{poison_groups}/o; |
| |
| |
| if (defined &local_filter_control) { |
| if (defined &local_filter_control) { |
| my @result = local_filter_control(); |
| my @result = local_filter_control(); |
| return reject(@result) if $result[0]; |
| return reject(@result) if $result[0]; |
| }; |
| }; |
| |
| |
| # other control messages ################################################# |
| # other control messages ################################################# |
| } elsif ($hdr{Control} =~ /^$hws*(\w+)(?:$hws+(.*))?/) { |
| } elsif ($hdr{Control} =~ /^$hws*(\w+)(?:$hws+(.*))?/) { |
| my $control_type = $1; |
| my $control_type = $1; |
| my $control_group = $2; |
| my $control_group = $2; |
| |
| |
| return reject("$control_type with Supersedes header") |
| return reject("$control_type with Supersedes header") |
| if $hdr{Supersedes}; |
| if $hdr{Supersedes}; |
| |
| |
| return reject("Obsolete $1 control message", 'Bad control message') |
| return reject("Obsolete $1 control message", 'Bad control message') |
| if $config{drop_useless_controls} |
| if $config{drop_useless_controls} |
| and $control_type =~ /^(sendsys|senduuname|version|whogets)$/; |
| and $control_type =~ /^(sendsys|senduuname|version|whogets)$/; |
| return reject("Unwanted $1 control message", 'Bad control message') |
| return reject("Unwanted $1 control message", 'Bad control message') |
| if $config{drop_ihave_sendme} |
| if $config{drop_ihave_sendme} |
| and $control_type =~ /^(ihave|sendme)$/; |
| and $control_type =~ /^(ihave|sendme)$/; |
| |
| |
| } |
| } |
| ########################################################################## |
| ########################################################################## |
| |
| |
| $status{accepted}++; |
| $status{accepted}++; |
| |
| policy_count('accepted', $state{peer}, $groups[0]) if $config{metrics_enabled};
|
| |
| policy_log_event('accept', 'accepted', '') if $config{policy_log_accepts};
|
| $timer{accepted}++ if $config{timer_info}; |
| $timer{accepted}++ if $config{timer_info}; |
| return ''; |
| return ''; |
| } |
| } |
| |
| |
| |
| # Build a bounded scan window. Binary markers normally occur near the start,
|
| |
| # but yEnc terminators and MIME part headers can also be useful near the end.
|
| |
| # Avoid copying an arbitrarily large article into another full-size scalar.
|
| |
| sub binary_scan_body {
|
| |
| my $raw = defined $hdr{__BODY__} ? $hdr{__BODY__} : '';
|
| |
| my $head_bytes = $config{binary_scan_bytes} || 262144;
|
| |
| my $tail_bytes = $config{binary_scan_tail_bytes} || 16384;
|
| |
|
|
| |
| return $raw if length($raw) <= $head_bytes + $tail_bytes;
|
| |
| return substr($raw, 0, $head_bytes) . "\n" . substr($raw, -$tail_bytes);
|
| |
| }
|
| |
|
|
| |
| sub set_binary_extension {
|
| |
| my ($filename) = @_;
|
| |
| return unless defined $filename;
|
| |
| $filename =~ s/^["']|["']$//g;
|
| |
| if ($filename =~ /\.([A-Za-z0-9]{1,16})\s*$/) {
|
| |
| $state{file_extension} = lc $1;
|
| |
| }
|
| |
| }
|
| |
|
|
| |
| # Return a descriptive string for a MIME binary attachment, or false.
|
| |
| sub mime_binary_type {
|
| |
| return 0 unless $config{detect_mime_binaries};
|
| |
|
|
| |
| my $scan = binary_scan_body();
|
| |
| my $top_ct = $hdr{'Content-Type'} || '';
|
| |
| my $top_cd = $hdr{'Content-Disposition'} || '';
|
| |
| my $top_ce = $hdr{'Content-Transfer-Encoding'} || '';
|
| |
|
|
| |
| # A top-level non-text MIME entity is binary regardless of its size.
|
| |
| if ($top_ct =~ m{^\s*(?:application|audio|video|image|model|font)/}i) {
|
| |
| if ($top_ct =~ /\bname\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {
|
| |
| set_binary_extension(defined $1 ? $1 : $2);
|
| |
| }
|
| |
| if ($top_cd =~ /\bfilename\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {
|
| |
| set_binary_extension(defined $1 ? $1 : $2);
|
| |
| }
|
| |
| return "MIME binary ($1)" if $top_ct =~ m{^\s*([^;\s]+)};
|
| |
| return 'MIME binary';
|
| |
| }
|
| |
|
|
| |
| # Multipart bodies carry their own MIME headers. Only classify a part as
|
| |
| # binary when its declared media type is non-text, or it is explicitly an
|
| |
| # attachment with a filename and a binary transfer encoding.
|
| |
| while ($scan =~ /(?:\A|\r?\n)Content-Type:\s*([^\r\n]+)(?:\r?\n[ \t]+[^\r\n]+)*\r?\n(?:(?:Content-(?:Transfer-Encoding|Disposition):[^\r\n]*(?:\r?\n[ \t]+[^\r\n]+)*\r?\n){0,3})/ig) {
|
| |
| my $block = $&;
|
| |
| my $ct = $1;
|
| |
| next if $ct =~ m{^\s*(?:text|message)/}i;
|
| |
| next unless $ct =~ m{^\s*(?:application|audio|video|image|model|font)/}i;
|
| |
| if ($block =~ /\b(?:name|filename)\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {
|
| |
| set_binary_extension(defined $1 ? $1 : $2);
|
| |
| }
|
| |
| return "MIME binary part ($1)" if $ct =~ m{^\s*([^;\s]+)};
|
| |
| return 'MIME binary part';
|
| |
| }
|
| |
|
|
| |
| # Some broken posters omit Content-Type on an attachment part.
|
| |
| if ($scan =~ /(?:\A|\r?\n)Content-Disposition:\s*attachment\b[^\r\n]*(?:\r?\n[ \t]+[^\r\n]+)*\r?\n/ig) {
|
| |
| my $block = $&;
|
| |
| if ($block =~ /\bfilename\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {
|
| |
| set_binary_extension(defined $1 ? $1 : $2);
|
| |
| }
|
| |
| return 'MIME attachment'
|
| |
| if $scan =~ /(?:\A|\r?\n)Content-Transfer-Encoding:\s*(?:base64|binary)\s*$/im;
|
| |
| }
|
| |
|
|
| |
| return 0;
|
| |
| }
|
| |
|
|
| # Return true if the article is a binary, false otherwise. |
| # Return true if the article is a binary, false otherwise. |
| sub is_binary { |
| sub is_binary { |
| return $state{cache_is_binary} if defined $state{cache_is_binary}; |
| return $state{cache_is_binary} if defined $state{cache_is_binary}; |
| |
| |
| # Return False if the article is Base64 encoded. We want to match binary |
| my $scan = binary_scan_body(); |
| # content, *not* Base64 encoding.
|
|
|
| if ($hdr{'Content-Transfer-Encoding'} =~ /^base64$/i |
| # MIME-declared binary payloads must be caught even when they are much |
| and $hdr{'Content-Type'} =~ /^text/i) { |
| # shorter than max_base64_lines. The old code missed these completely. |
| $state{cache_is_binary} = 0; |
| my $mime_type = mime_binary_type(); |
| |
| if ($mime_type) {
|
| |
| $state{cache_is_binary} = $mime_type;
|
| return $state{cache_is_binary}; |
| return $state{cache_is_binary}; |
| }; |
| } |
| |
| |
| # yEnc checks: According to yEnc spec, all encoded parts start with |
| # A base64-encoded text/* article is text, not a misplaced binary. |
| # =ybegin. |
| if (($hdr{'Content-Transfer-Encoding'} || '') =~ /^\s*base64\s*$/i |
| if ($body =~ /(?:^|\n)=ybegin$hws+(.+)/o) { |
| and ($hdr{'Content-Type'} || '') =~ /^\s*text\//i) { |
| local $_ = $1; |
| $state{cache_is_binary} = 0; |
| if (/line=/ and /size=/ and /name=/) { |
| return 0; |
| $state{cache_is_binary} = "yEnc Encoded"; |
| } |
| return $state{cache_is_binary};
|
| |
| };
|
| |
| };
|
| |
| |
| |
| # uuEncoded check |
| # yEnc. Scan the raw body rather than the lower-cased 4 kB body preview. |
| if ($body =~ /(?:^|\n)begin$hws+(.+)/io) { |
| # Accept attribute order variations and common non-compliant generators. |
| local $_ = $1; |
| if ($scan =~ /(?:\A|\r?\n)[ \t]*=ybegin\b([^\r\n]*)/i) { |
| if (/[0-7]{3,4}$hws.*\.(\w{2,4})/o) { |
| my $attrs = $1; |
| $state{file_extension} = $1; |
| my ($name) = $attrs =~ /\bname\s*=\s*(.+?)\s*$/i; |
| $state{cache_is_binary} = "uuencoded"; |
| set_binary_extension($name) if defined $name; |
| |
|
|
| |
| my $has_size = $attrs =~ /\bsize\s*=\s*\d+/i;
|
| |
| my $has_line = $attrs =~ /\bline\s*=\s*\d+/i;
|
| |
| my $has_name = $attrs =~ /\bname\s*=\s*\S+/i;
|
| |
| my $has_part = $attrs =~ /\bpart\s*=\s*\d+/i
|
| |
| || $scan =~ /(?:\A|\r?\n)[ \t]*=ypart\b[^\r\n]*/i;
|
| |
| my $has_end = $scan =~ /(?:\A|\r?\n)[ \t]*=yend\b[^\r\n]*/i;
|
| |
|
|
| |
| if (($has_size && $has_name && ($has_line || $has_part || $has_end))
|
| |
| || ($config{detect_malformed_yenc} && ($has_end || $has_part))) {
|
| |
| $state{cache_is_binary} = $has_part ? 'yEnc multipart' : 'yEnc encoded';
|
| return $state{cache_is_binary}; |
| return $state{cache_is_binary}; |
| }; |
| } |
| }; |
| } |
| |
|
|
| |
| # Catch malformed/case-damaged yEnc produced by a few old or custom
|
| |
| # posters, while requiring a second marker to reduce false positives in
|
| |
| # discussions that merely mention "=ybegin".
|
| |
| if ($config{detect_malformed_yenc}
|
| |
| and $scan =~ /(?:\A|\r?\n)[ \t]*=ybegin\b/i
|
| |
| and $scan =~ /(?:\A|\r?\n)[ \t]*=y(?:part|end)\b/i) {
|
| |
| $state{cache_is_binary} = 'Malformed yEnc';
|
| |
| return $state{cache_is_binary};
|
| |
| }
|
| |
| |
| if ($hdr{__BODY__} =~ / |
| # uuEncoded check. |
| |
| if ($scan =~ /(?:\A|\r?\n)[ \t]*begin$hws+([0-7]{3,4})$hws+([^\r\n]+)/io) {
|
| |
| set_binary_extension($2);
|
| |
| $state{cache_is_binary} = 'uuencoded';
|
| |
| return $state{cache_is_binary};
|
| |
| }
|
| |
|
|
| |
| if ($scan =~ /
|
| (?: |
| (?: |
| [ \t|>]* # Skip Quote Marks
|
| [ \t|>]*
|
| (?> # Disable Backtracking |
| (?>M[\x20-\x60]{60,61}) |
| M[\x20-\x60]{60,61} # uuencoded line |
| $hws*\x0D?\x0A |
| )$hws*\x0D?\x0A # End of line |
| ){4} |
| ){4} # Require at least four encoded lines
|
| |
| /ox) { |
| /ox) { |
| $state{cache_is_binary} = "uuencoded multi-part"; |
| $state{cache_is_binary} = 'uuencoded multi-part'; |
| return $state{cache_is_binary}; |
| return $state{cache_is_binary}; |
| }; |
| } |
| |
| |
| # We only need to count Base64 lines if the number of lines in the post |
| # Heuristic for raw Base64 without MIME headers. |
| # exceeds the configured Base64 maximum allowed.
|
| |
| if ($state{lines} > $config{max_base64_lines}) { |
| if ($state{lines} > $config{max_base64_lines}) { |
| $state{b64lines} = 0; |
| $state{b64lines} = 0; |
| $state{b64lines}++ |
| $state{b64lines}++ |
| while $hdr{__BODY__} =~ /^$hws*[A-Za-z0-9\+\/]{59,76}$hws*$/gmo; |
| while $scan =~ /^$hws*[A-Za-z0-9+\/]{59,76}={0,2}$hws*$/gmo; |
| if ($state{b64lines} > $config{max_base64_lines}) { |
| if ($state{b64lines} > $config{max_base64_lines}) { |
| $state{cache_is_binary} = "Base64 ($state{b64lines} lines)"; |
| $state{cache_is_binary} = "Base64 ($state{b64lines} lines)"; |
| return $state{cache_is_binary}; |
| return $state{cache_is_binary}; |
| }; |
| } |
| }; |
| } |
| |
| |
| # Message is not binary
|
| |
| $state{cache_is_binary} = 0; |
| $state{cache_is_binary} = 0; |
| return 0; |
| return 0; |
| }; |
| }; |
| |
| |
| # Extract the right-most FQDN from a Path header |
| # Extract the right-most FQDN from a Path header |
| sub first_path_host{ |
| sub first_path_host{ |
| local $_ = shift; |
| local $_ = shift; |
| s/(![^\.]+)+$//; # Strip RH non-FQDNs |
| s/(![^\.]+)+$//; # Strip RH non-FQDNs |
| s/!\.POSTED.*//; # Strip diagnostic host entry |
| s/!\.POSTED.*//; # Strip diagnostic host entry |
| s/.*!//; # Strip all but RH path entry |
| s/.*!//; # Strip all but RH path entry |
| return $_; |
| return $_; |
| }; |
| }; |
| |
| |
| # Attempt to determine the client software |
| # Attempt to determine the client software |
| sub x_reader { |
| sub x_reader { |
| return lc $hdr{'X-Newsreader'} || |
| return lc $hdr{'X-Newsreader'} || |
| lc $hdr{'User-Agent'} || |
| lc $hdr{'User-Agent'} || |
| lc $hdr{'X-Newsposter'} || |
| lc $hdr{'X-Newsposter'} || |
| lc $hdr{'X-Poster'} || |
| lc $hdr{'X-Poster'} || |
| lc $hdr{'X-Mailer'} || ''; |
| lc $hdr{'X-Mailer'} || ''; |
| } |
| } |
| |
| |
| sub reject { |
| sub reject { |
| my ($verbose_reason, $short_reason) = @_; |
| my ($verbose_reason, $short_reason) = @_; |
| |
| |
| if (defined &local_filter_reject) { |
| if (defined &local_filter_reject) { |
| ($verbose_reason, $short_reason) = local_filter_reject(@_); |
| ($verbose_reason, $short_reason) = local_filter_reject(@_); |
| return if not $verbose_reason; |
| return if not $verbose_reason; |
| } |
| } |
| |
| |
| $short_reason = $verbose_reason unless $short_reason; |
| $short_reason = $verbose_reason unless $short_reason; |
| |
| my $rule = policy_reason_key($short_reason || $verbose_reason);
|
| |
| if (trusted_bypass_for_rule($rule)) {
|
| |
| policy_count('trusted.bypass', $state{peer}, $groups[0]) if $config{metrics_enabled};
|
| |
| policy_log_event('bypass', $rule, $verbose_reason) if $config{policy_log_matches};
|
| |
| return '';
|
| |
| }
|
| |
| |
| if ($config{block_late_cancels} |
| if ($config{block_late_cancels} |
| # XXX $config{block_extra_reposts} |
| # XXX $config{block_extra_reposts} |
| # XXX for reposts and not $hdr{Control} |
| # XXX for reposts and not $hdr{Control} |
| ) { |
| ) { |
| $MIDhistory->add($hdr{'Message-ID'}); |
| $MIDhistory->add($hdr{'Message-ID'}); |
| } |
| } |
| |
| |
| $status{rejected}++; |
| $status{rejected}++; |
| |
| policy_count($rule, $state{peer}, $groups[0]) if $config{metrics_enabled};
|
| |
| policy_log_event('reject', $rule, $verbose_reason) if $config{policy_log_matches};
|
| |
| |
| return $config{verbose} ? $verbose_reason : $short_reason; |
| record_top_offender($rule); |
| |
| my $code = reject_code($rule);
|
| |
| return $config{verbose} ? "[$code] $verbose_reason" : "[$code] $short_reason";
|
| |
| }
|
| |
|
|
| |
|
|
| |
|
|
| |
| ##############################################################################
|
| |
| # Lightweight operational hardening, diagnostics, and anomaly detection
|
| |
| ##############################################################################
|
| |
| sub reject_code {
|
| |
| my ($rule) = @_;
|
| |
| my %codes = (
|
| |
| 'binary.yenc'=>'CF-BINARY-YENC','binary.uuencode'=>'CF-BINARY-UUENCODE',
|
| |
| 'binary.base64'=>'CF-BINARY-BASE64','binary.mime'=>'CF-BINARY-MIME',
|
| |
| 'binary.byte_profile'=>'CF-BINARY-BYTE-PROFILE','policy.binary'=>'CF-POLICY-BINARY',
|
| |
| 'policy.size'=>'CF-POLICY-SIZE','crosspost'=>'CF-CROSSPOST',
|
| |
| 'malformed.encoding'=>'CF-MALFORMED-ENCODING','structure.long_line'=>'CF-LONG-LINE',
|
| |
| 'structure.path'=>'CF-PATH-SANITY','anomaly.rate'=>'CF-RATE-ANOMALY',
|
| |
| 'emp.md5'=>'CF-EMP-MD5','emp.phl'=>'CF-EMP-PHL','emp.phn'=>'CF-EMP-PHN',
|
| |
| 'emp.phr'=>'CF-EMP-PHR','emp.fsl'=>'CF-EMP-FSL','header.subject'=>'CF-BAD-SUBJECT',
|
| |
| 'header.from'=>'CF-BAD-FROM','body.pattern'=>'CF-BAD-BODY','body.url'=>'CF-BAD-URL',
|
| |
| 'supersedes'=>'CF-SUPERSEDES','control'=>'CF-CONTROL','scoring'=>'CF-SCORING',
|
| |
| 'message_id'=>'CF-MESSAGE-ID','other'=>'CF-OTHER'
|
| |
| );
|
| |
| return $codes{$rule} || 'CF-' . uc(policy_safe_key($rule || 'other'));
|
| |
| }
|
| |
|
|
| |
| sub external_regex_safe {
|
| |
| my ($text, $file) = @_;
|
| |
| return 1 unless $config{external_regex_reject_unsafe};
|
| |
| return 0 if length($text) > $config{external_regex_max_length};
|
| |
| my $count = 1 + ($text =~ tr/|/|/);
|
| |
| return 0 if $count > $config{external_regex_max_count};
|
| |
| return 0 if $text =~ /\(\?\??\{/; # executable Perl regex code
|
| |
| return 0 if $text =~ /(?:\*|\+|\{\d+,?\})\s*\)(?:\*|\+|\{\d+,?\})/;
|
| |
| return 0 if $text =~ /\([^)]*(?:\.\*|\.\+)[^)]*\)(?:\*|\+)/;
|
| |
| return 1;
|
| |
| }
|
| |
|
|
| |
| sub bad_file_names {
|
| |
| return qw(bad_paths bad_cancel_paths bad_adult_paths bad_hosts bad_hosts_central
|
| |
| bad_from bad_subject bad_body bad_url bad_url_central trusted_paths
|
| |
| trusted_hosts trusted_from trusted_message_ids);
|
| |
| }
|
| |
| sub update_bad_file_mtimes {
|
| |
| return unless $config_dir;
|
| |
| for my $f (bad_file_names()) { my $p="$config_dir/$f"; $Bad_File_Mtime{$f}=(-e $p ? (stat($p))[9]||0 : 0); }
|
| |
| }
|
| |
| sub bad_files_changed {
|
| |
| return 0 unless $config_dir;
|
| |
| for my $f (bad_file_names()) { my $p="$config_dir/$f"; my $m=(-e $p ? (stat($p))[9]||0 : 0); return 1 if !defined($Bad_File_Mtime{$f}) || $m != $Bad_File_Mtime{$f}; }
|
| |
| return 0;
|
| |
| }
|
| |
| sub maybe_reload_bad_files {
|
| |
| my $mode=$config{bad_reload_mode}||'articles';
|
| |
| my $reload=0;
|
| |
| if (($mode eq 'mtime'||$mode eq 'both') && $now-$Last_Bad_Mtime_Check >= $config{bad_reload_interval}) {
|
| |
| $Last_Bad_Mtime_Check=$now; $reload=1 if bad_files_changed();
|
| |
| }
|
| |
| if (($mode eq 'articles'||$mode eq 'both') && $status{accepted}>0 && $config{bad_rate_reload}>0
|
| |
| && $status{accepted}%$config{bad_rate_reload}==0 && $status{accepted}>($status{bad_reloaded}||0)) {
|
| |
| $status{bad_reloaded}=$status{accepted}; $reload=1;
|
| |
| }
|
| |
| if ($reload) { slog('N','Reloading external cleanfeed lists'); read_hashes(); }
|
| |
| }
|
| |
|
|
| |
| sub update_config_fingerprint {
|
| |
| return unless eval { require Digest::SHA; 1 };
|
| |
| my $sha=Digest::SHA->new(256); $sha->add($cleanfeed_maintenance_version);
|
| |
| if ($config_dir) { for my $f ('cleanfeed.local',bad_file_names()) { my $p="$config_dir/$f"; next unless -f $p; if(open my $fh,'<',$p){binmode $fh;$sha->addfile($fh);close $fh;} } }
|
| |
| $Config_Fingerprint='sha256:'.$sha->hexdigest;
|
| |
| }
|
| |
|
|
| |
| sub normalize_domain {
|
| |
| my ($d)=@_; $d=lc($d||''); $d =~ s/^\s+|\s+$//g;
|
| |
| if ($config{idn_normalization} && eval { require Net::IDN::Encode; 1 }) { eval { $d=Net::IDN::Encode::domain_to_ascii($d); }; }
|
| |
| return $d;
|
| |
| }
|
| |
| sub trusted_article {
|
| |
| return 1 if $Trusted_Paths_RE && ($hdr{Path}||'') =~ $Trusted_Paths_RE;
|
| |
| return 1 if $Trusted_Hosts_RE && (($state{posting_host}||'') =~ $Trusted_Hosts_RE || ($state{peer}||'') =~ $Trusted_Hosts_RE);
|
| |
| return 1 if $Trusted_From_RE && ($hdr{From}||'') =~ $Trusted_From_RE;
|
| |
| return 1 if $Trusted_Message_IDs_RE && ($hdr{'Message-ID'}||'') =~ $Trusted_Message_IDs_RE;
|
| |
| return 0;
|
| |
| }
|
| |
| sub trusted_bypass_for_rule {
|
| |
| my ($r)=@_; return 0 unless trusted_article();
|
| |
| return $config{trusted_bypass_scoring} if $r =~ /^(?:scoring|emp\.)/;
|
| |
| return $config{trusted_bypass_content} if $r =~ /^(?:header\.|body\.|malformed)/;
|
| |
| return $config{trusted_bypass_binary} if $r =~ /^(?:binary\.|policy\.binary)/;
|
| |
| return $config{trusted_bypass_crosspost} if $r eq 'crosspost';
|
| |
| return $config{trusted_bypass_size} if $r eq 'policy.size';
|
| |
| return 0;
|
| |
| }
|
| |
|
|
| |
| sub mode_result {
|
| |
| my ($mode,$rule,$reason)=@_; return '' if !$mode || $mode eq 'off';
|
| |
| if ($mode eq 'audit') { $status{audited}++; policy_count($rule,$state{peer},$groups[0]); policy_log_event('audit',$rule,$reason); return ''; }
|
| |
| return reject($reason,$rule) if $mode eq 'reject'; return '';
|
| |
| }
|
| |
| sub max_line_length {
|
| |
| my ($text)=@_; my $max=0; for my $line (split(/\n/,$text||'',-1)){ my $l=length($line);$max=$l if $l>$max;} return $max;
|
| |
| }
|
| |
| sub malformed_encoding_reason {
|
| |
| return '' unless $config{malformed_encoding_check}; my $scan=binary_scan_body();
|
| |
| my $ct=$hdr{'Content-Type'}||''; my $cte=$hdr{'Content-Transfer-Encoding'}||'';
|
| |
| return 'Malformed MIME multipart: boundary parameter missing' if $ct =~ /^multipart\//i && $ct !~ /boundary\s*=/i;
|
| |
| if ($ct =~ /boundary\s*=\s*"?([^";\s]+)/i) { my $b=$1; return 'Malformed MIME multipart: declared boundary not found' if index($scan,"--$b")<0; }
|
| |
| return 'Malformed Base64 transfer encoding: no plausible payload' if $cte =~ /^base64$/i && $scan !~ /(?:^|\n)[A-Za-z0-9+\/]{16,}={0,2}\r?$/m;
|
| |
| my $yb=()=$scan=~/(?:^|\n)\s*=ybegin\b/ig; my $ye=()=$scan=~/(?:^|\n)\s*=yend\b/ig;
|
| |
| return 'Malformed yEnc: =ybegin without =yend' if $yb && !$ye;
|
| |
| return 'Malformed yEnc: =yend without =ybegin' if $ye && !$yb;
|
| |
| my ($ybegin_size) = $scan =~ /(?:^|\n)\s*=ybegin\b[^\r\n]*\bsize=(\d+)/i;
|
| |
| my ($yend_size) = $scan =~ /(?:^|\n)\s*=yend\b[^\r\n]*\bsize=(\d+)/i;
|
| |
| if (defined $ybegin_size && defined $yend_size) {
|
| |
| my($a,$b)=($ybegin_size,$yend_size); my $tol=$config{yenc_size_tolerance_percent}; return "Malformed yEnc: inconsistent declared sizes $a/$b" if $a && abs($a-$b)*100/$a > $tol;
|
| |
| }
|
| |
| return '';
|
| |
| }
|
| |
| sub byte_profile_reason {
|
| |
| return '' unless $config{binary_byte_profile_enabled}; my $raw=substr($hdr{__BODY__}||'',0,$config{binary_ratio_scan_bytes});
|
| |
| return '' if length($raw)<$config{binary_ratio_min_bytes}; my $bad=($raw =~ tr/\x00-\x08\x0b\x0c\x0e-\x1f\x7f//);
|
| |
| my $pct=int($bad*100/length($raw)); return $pct>$config{binary_nonprintable_ratio_percent} ? "Binary byte profile: nonprintable ratio ${pct}%" : '';
|
| |
| }
|
| |
| sub path_sanity_reason {
|
| |
| return '' unless $config{path_sanity_enabled}; my @p=split(/!/,$hdr{Path}||'',-1);
|
| |
| return 'Path sanity: excessive hop count' if @p>$config{path_max_hops}; my %n;
|
| |
| for my $t(@p){ return 'Path sanity: empty token' if $t eq ''; return 'Path sanity: token too long' if length($t)>$config{path_max_token_bytes}; $n{lc$t}++; return 'Path sanity: repeated token' if $n{lc$t}>$config{path_repeat_ceiling}; }
|
| |
| return '';
|
| |
| }
|
| |
| sub rate_anomaly_reason {
|
| |
| return '' unless $config{anomaly_rate_enabled}; my $bucket=int($now/$config{anomaly_window});
|
| |
| for my $x ([$state{peer}||'unknown',\%rate_peer,$config{anomaly_peer_articles},'peer'],[$state{posting_host}||'unknown',\%rate_host,$config{anomaly_host_articles},'posting host']) {
|
| |
| my($k,$h,$lim,$label)=@$x; next if !$lim || !$k||$k eq 'unknown'||$k eq '0'; %$h=() if scalar(keys %$h)>$config{anomaly_max_keys}; my $id="$bucket\0$k"; $h->{$id}++; return "Rate anomaly: $label exceeded $lim articles per window" if $h->{$id}>$lim;
|
| |
| } return '';
|
| |
| }
|
| |
| sub apply_lightweight_guards {
|
| |
| my $mh=max_line_length(join("\n",map { ($_||'') } values %hdr));
|
| |
| if($config{max_header_line_bytes} && $mh>$config{max_header_line_bytes}){ my$r=mode_result($config{long_line_mode},'structure.long_line',"Header line exceeds $config{max_header_line_bytes} bytes");return$r if $r; }
|
| |
| my $mb=max_line_length($hdr{__BODY__}||'');
|
| |
| if($config{max_body_line_bytes} && $mb>$config{max_body_line_bytes}){ my$r=mode_result($config{long_line_mode},'structure.long_line',"Body line exceeds $config{max_body_line_bytes} bytes");return$r if $r; }
|
| |
| for my $c ([malformed_encoding_reason(),$config{malformed_encoding_mode},'malformed.encoding'],[byte_profile_reason(),$config{binary_byte_profile_mode},'binary.byte_profile'],[path_sanity_reason(),$config{path_sanity_mode},'structure.path'],[rate_anomaly_reason(),$config{anomaly_mode},'anomaly.rate']) { my($reason,$mode,$rule)=@$c; next unless $reason; my$r=mode_result($mode,$rule,$reason);return$r if $r; }
|
| |
| return '';
|
| |
| }
|
| |
| sub record_top_offender {
|
| |
| my($rule)=@_; return unless $config{top_offenders_enabled};
|
| |
| for my $x ([\%top_peer_reject,$state{peer}],[\%top_host_reject,$state{posting_host}],[\%top_group_reject,$groups[0]]) { my($h,$k)=@$x;next unless $k; %$h=() if scalar(keys %$h)>$config{top_offenders_max_keys};$h->{policy_safe_key($k)}++; }
|
| |
| }
|
| |
| sub append_top_metrics {
|
| |
| my($lines)=@_; return unless $config{top_offenders_enabled};
|
| |
| for my $set (['top_reject_peer',\%top_peer_reject],['top_reject_host',\%top_host_reject],['top_reject_group',\%top_group_reject]) { my($prefix,$h)=@$set;my$i=0;for my $k(sort{$h->{$b}<=>$h->{$a}||$a cmp $b}keys%$h){last if ++$i>$config{top_offenders_limit};push @$lines,"${prefix}_${i}=$k,$h->{$k}";} }
|
| |
| }
|
| |
|
|
| |
| ##############################################################################
|
| |
| # Peer/hierarchy policy engine and lightweight metrics
|
| |
| ##############################################################################
|
| |
| sub policy_peer_identity {
|
| |
| return $state{injection_host} if $state{injection_host};
|
| |
| return first_path_host($hdr{Path}) if $hdr{Path};
|
| |
| return 'unknown';
|
| |
| }
|
| |
|
|
| |
| sub policy_merge {
|
| |
| my ($base, $extra) = @_;
|
| |
| my %merged = %{$base || {}};
|
| |
| @merged{keys %$extra} = values %$extra if $extra;
|
| |
| return \%merged;
|
| |
| }
|
| |
|
|
| |
| sub policy_for_article {
|
| |
| my $policy = {
|
| |
| mode => $config{policy_mode} || 'audit',
|
| |
| max_bytes => $config{policy_default_max_bytes} || 0,
|
| |
| allow_binary => $config{policy_default_allow_binary} ? 1 : 0,
|
| |
| };
|
| |
| my $peer = $state{peer} || 'unknown';
|
| |
| for my $pattern (sort { length($a) <=> length($b) || $a cmp $b } keys %Peer_Policies) {
|
| |
| next unless $peer =~ /$pattern/;
|
| |
| $policy = policy_merge($policy, $Peer_Policies{$pattern});
|
| |
| }
|
| |
| for my $group (@groups) {
|
| |
| for my $pattern (sort { length($a) <=> length($b) || $a cmp $b } keys %Hierarchy_Policies) {
|
| |
| next unless $group =~ /$pattern/;
|
| |
| $policy = policy_merge($policy, $Hierarchy_Policies{$pattern});
|
| |
| }
|
| |
| }
|
| |
| return $policy;
|
| |
| }
|
| |
|
|
| |
| sub apply_policy_engine {
|
| |
| my $policy = policy_for_article();
|
| |
| my @matches;
|
| |
| if ($policy->{max_bytes} && $state{article_bytes} > $policy->{max_bytes}) {
|
| |
| push @matches, ['policy.size', "Article size $state{article_bytes} exceeds policy limit $policy->{max_bytes}"];
|
| |
| }
|
| |
| if (defined $policy->{allow_binary} && !$policy->{allow_binary}) {
|
| |
| my $binary = is_binary();
|
| |
| push @matches, ['policy.binary', 'Binary payload forbidden by peer/hierarchy policy'] if $binary;
|
| |
| }
|
| |
| return '' unless @matches;
|
| |
|
|
| |
| my $mode = $policy->{mode} || $config{policy_mode} || 'audit';
|
| |
| return '' if $mode eq 'off';
|
| |
| for my $match (@matches) {
|
| |
| my ($rule, $reason) = @$match;
|
| |
| policy_count($rule, $state{peer}, $groups[0]) if $mode ne 'reject';
|
| |
| policy_log_event($mode, $rule, $reason) if $config{policy_log_matches};
|
| |
| }
|
| |
| if ($mode eq 'audit') {
|
| |
| $status{audited}++;
|
| |
| return '';
|
| |
| }
|
| |
| if ($mode eq 'quarantine') {
|
| |
| $status{quarantined}++;
|
| |
| # Logical quarantine: article is accepted but distinctly logged/counted.
|
| |
| # Storage or routing decisions belong in INN feed configuration.
|
| |
| return '';
|
| |
| }
|
| |
| return reject($matches[0][1], $matches[0][0]) if $mode eq 'reject';
|
| |
| return '';
|
| |
| }
|
| |
|
|
| |
| sub policy_reason_key {
|
| |
| my ($reason) = @_;
|
| |
| $reason = lc($reason || 'other');
|
| |
| return 'binary.yenc' if $reason =~ /yenc/;
|
| |
| return 'binary.uuencode' if $reason =~ /uuenc/;
|
| |
| return 'binary.base64' if $reason =~ /base64/;
|
| |
| return 'binary.mime' if $reason =~ /mime|binary payload|misplaced binary/;
|
| |
| return 'crosspost' if $reason =~ /crosspost|too many newsgroups|poison newsgroup/;
|
| |
| return 'emp.md5' if $reason =~ /md5/;
|
| |
| return 'emp.phl' if $reason =~ /phl/;
|
| |
| return 'emp.phn' if $reason =~ /phn/;
|
| |
| return 'emp.phr' if $reason =~ /phr/;
|
| |
| return 'emp.fsl' if $reason =~ /fsl/;
|
| |
| return 'header.subject' if $reason =~ /subject/;
|
| |
| return 'header.from' if $reason =~ /from|sender|reply-to/;
|
| |
| return 'body.pattern' if $reason =~ /body/;
|
| |
| return 'body.url' if $reason =~ /url/;
|
| |
| return 'supersedes' if $reason =~ /supersedes/;
|
| |
| return 'control' if $reason =~ /control|newgroup|rmgroup/;
|
| |
| return 'scoring' if $reason =~ /scoring/;
|
| |
| return 'message_id' if $reason =~ /message-id/;
|
| |
| return 'malformed.encoding' if $reason =~ /malformed|boundary|encoding/;
|
| |
| return 'structure.long_line' if $reason =~ /line.*bytes|long line/;
|
| |
| return 'structure.path' if $reason =~ /path sanity|path token|path hop|path repeat/;
|
| |
| return 'binary.byte_profile' if $reason =~ /nonprintable|byte profile/;
|
| |
| return 'anomaly.rate' if $reason =~ /rate anomaly/;
|
| |
| return 'other';
|
| |
| }
|
| |
|
|
| |
| sub policy_safe_key {
|
| |
| my ($value) = @_;
|
| |
| $value = lc($value || 'unknown');
|
| |
| $value =~ s/[^a-z0-9_.-]+/_/g;
|
| |
| return substr($value, 0, 120);
|
| |
| }
|
| |
|
|
| |
| sub policy_count {
|
| |
| my ($rule, $peer, $hierarchy) = @_;
|
| |
| $rule = policy_safe_key($rule);
|
| |
| $policy_rule_count{$rule}++ if $config{metrics_by_rule};
|
| |
| if ($config{metrics_by_peer} && scalar(keys %policy_peer_count) < $config{policy_max_peer_counters}) {
|
| |
| $policy_peer_count{policy_safe_key($peer)}++;
|
| |
| }
|
| |
| if ($config{metrics_by_hierarchy} && scalar(keys %policy_hierarchy_count) < $config{policy_max_hierarchy_counters}) {
|
| |
| my ($root) = split(/\./, $hierarchy || 'unknown', 2);
|
| |
| $policy_hierarchy_count{policy_safe_key($root)}++;
|
| |
| }
|
| |
| }
|
| |
|
|
| |
| sub policy_log_event {
|
| |
| my ($action, $rule, $reason) = @_;
|
| |
| my @parts = ('cleanfeed_event', "action=" . policy_safe_key($action), "rule=" . policy_safe_key($rule));
|
| |
| push @parts, 'peer=' . policy_safe_key($state{peer}) if $config{policy_include_peer};
|
| |
| push @parts, 'groups=' . policy_safe_key(join(',', @groups)) if $config{policy_include_groups};
|
| |
| push @parts, 'message_id=' . policy_safe_key($hdr{'Message-ID'}) if $config{policy_include_message_id};
|
| |
| my $safe_reason = $reason || '';
|
| |
| $safe_reason =~ s/[\r\n\t]+/ /g;
|
| |
| $safe_reason =~ s/\s+/ /g;
|
| |
| push @parts, 'reason="' . substr($safe_reason, 0, 240) . '"' if $safe_reason ne '';
|
| |
| slog('N', join(' ', @parts));
|
| |
| }
|
| |
|
|
| |
| sub atomic_write_file {
|
| |
| my ($path, $content) = @_;
|
| |
| return 1 if not $path;
|
| |
| my $tmp = "$path.tmp.$$";
|
| |
| my $fh;
|
| |
| if (!open($fh, '>', $tmp)) { slog('E', "Cannot open $tmp: $!"); return 0; }
|
| |
| print {$fh} $content;
|
| |
| if (!close($fh)) { unlink $tmp; slog('E', "Cannot close $tmp: $!"); return 0; }
|
| |
| if (!rename($tmp, $path)) { unlink $tmp; slog('E', "Cannot rename $tmp to $path: $!"); return 0; }
|
| |
| return 1;
|
| |
| }
|
| |
|
|
| |
| sub metric_snapshot_lines {
|
| |
| my @lines;
|
| |
| push @lines, "cleanfeed_version=$cleanfeed_maintenance_version";
|
| |
| push @lines, "generated_epoch=$now";
|
| |
| push @lines, "started_epoch=$Start_Time";
|
| |
| push @lines, 'uptime_seconds=' . ($now - $Start_Time);
|
| |
| for my $key (qw(articles accepted rejected refused audited quarantined)) {
|
| |
| push @lines, "$key=" . ($status{$key} || 0);
|
| |
| }
|
| |
| push @lines, "rate_articles_per_second=" . ($timer{rate} || 0);
|
| |
| push @lines, "rate_accepted_per_second=" . ($timer{accept_rate} || 0);
|
| |
| push @lines, "configuration_fingerprint=$Config_Fingerprint" if $Config_Fingerprint;
|
| |
| append_top_metrics(\@lines);
|
| |
| push @lines, "reject_rule_$_=$policy_rule_count{$_}" for sort keys %policy_rule_count;
|
| |
| push @lines, "peer_$_=$policy_peer_count{$_}" for sort keys %policy_peer_count;
|
| |
| push @lines, "hierarchy_$_=$policy_hierarchy_count{$_}" for sort keys %policy_hierarchy_count;
|
| |
| return join("\n", @lines) . "\n";
|
| |
| }
|
| |
|
|
| |
| sub write_metrics {
|
| |
| return unless $config{metrics_enabled};
|
| |
| atomic_write_file($config{metrics_status_file}, metric_snapshot_lines()) if $config{metrics_status_file};
|
| |
| if ($config{metrics_csv_file} && $now - $Last_Metrics_CSV >= $config{metrics_csv_interval}) {
|
| |
| my $new = !-e $config{metrics_csv_file} || -z $config{metrics_csv_file};
|
| |
| my $csv;
|
| |
| if (open($csv, '>>', $config{metrics_csv_file})) {
|
| |
| print {$csv} "epoch,articles,accepted,rejected,refused,audited,quarantined\n" if $new;
|
| |
| print {$csv} join(',', $now, map { $status{$_} || 0 } qw(articles accepted rejected refused audited quarantined)) . "\n";
|
| |
| close($csv);
|
| |
| $Last_Metrics_CSV = $now;
|
| |
| } else { slog('E', "Cannot append $config{metrics_csv_file}: $!"); }
|
| |
| }
|
| |
| if ($config{metrics_syslog}) {
|
| |
| slog('N', "metrics articles=" . ($status{articles}||0) . " accepted=" . ($status{accepted}||0) . " rejected=" . ($status{rejected}||0) . " audited=" . ($status{audited}||0) . " quarantined=" . ($status{quarantined}||0));
|
| |
| }
|
| |
| if ($config{metrics_prometheus_file}) {
|
| |
| my @prom = (
|
| |
| "cleanfeed_articles_total " . ($status{articles}||0),
|
| |
| "cleanfeed_articles_accepted_total " . ($status{accepted}||0),
|
| |
| "cleanfeed_articles_rejected_total " . ($status{rejected}||0),
|
| |
| "cleanfeed_articles_audited_total " . ($status{audited}||0),
|
| |
| "cleanfeed_articles_quarantined_total " . ($status{quarantined}||0),
|
| |
| );
|
| |
| push @prom, 'cleanfeed_reject_rule_total{rule="' . $_ . '"} ' . $policy_rule_count{$_} for sort keys %policy_rule_count;
|
| |
| atomic_write_file($config{metrics_prometheus_file}, join("\n", @prom) . "\n");
|
| |
| }
|
| } |
| } |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # other functions called by INN |
| # other functions called by INN |
| ############################################################################## |
| ############################################################################## |
| |
| |
| # examine message-id during CHECK and IHAVE transactions (INN only) |
| # examine message-id during CHECK and IHAVE transactions (INN only) |
| sub filter_messageid { |
| sub filter_messageid { |
| return '' if not $config{do_mid_filter}; |
| return '' if not $config{do_mid_filter}; |
| my ($id) = @_; |
| my ($id) = @_; |
| |
| |
| if ($config{refuse_messageids} and $id =~ /$config{refuse_messageids}/io) { |
| if ($config{refuse_messageids} and $id =~ /$config{refuse_messageids}/io) { |
| $status{refused}++; |
| $status{refused}++; |
| return 'No'; |
| return 'No'; |
| } |
| } |
| |
| |
| if ($config{block_late_cancels} |
| if ($config{block_late_cancels} |
| and (($id =~ /^<cancel\.[a-z0-9]{4}\.(.+)/ |
| and (($id =~ /^<cancel\.[a-z0-9]{4}\.(.+)/ |
| and $MIDhistory->check('<'.$1)) |
| and $MIDhistory->check('<'.$1)) |
| or ($id =~ /^<cancel\.(.+)/ and $MIDhistory->check('<'.$1)))) { |
| or ($id =~ /^<cancel\.(.+)/ and $MIDhistory->check('<'.$1)))) { |
| $status{refused}++; |
| $status{refused}++; |
| return 'No'; |
| return 'No'; |
| } |
| } |
| |
| |
| return ''; |
| return ''; |
| } |
| } |
| |
| |
| sub filter_mode { |
| sub filter_mode { |
| if ($config{do_emp_dump}) { |
| if ($config{do_emp_dump}) { |
| if ($mode{NewMode} eq 'throttled') { |
| if ($mode{NewMode} eq 'throttled') { |
| dump_emp(); |
| dump_emp(); |
| } elsif ($mode{NewMode} eq 'running') { |
| } elsif ($mode{NewMode} eq 'running') { |
| restore_emp() if $mode{Mode} eq 'throttled'; |
| restore_emp() if $mode{Mode} eq 'throttled'; |
| } |
| } |
| } |
| } |
| |
| |
| slog('N', 'Meow unto the greatness of Fluffy, Ruler of All Usenet') |
| slog('N', 'Meow unto the greatness of Fluffy, Ruler of All Usenet') |
| if lc $mode{reason} eq 'meow'; |
| if lc $mode{reason} eq 'meow'; |
| |
| |
| return; |
| return; |
| } |
| } |
| |
| |
| # a status line in "ctlinnd mode" output (INN only). |
| # a status line in "ctlinnd mode" output (INN only). |
| # (requires the "mode.patch" to innd or equivalent). |
| # (requires the "mode.patch" to innd or equivalent). |
| sub filter_stats { |
| sub filter_stats { |
| my $md5hashentries = $MD5history ? $MD5history->count : 0; |
| my $md5hashentries = $MD5history ? $MD5history->count : 0; |
| my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0; |
| my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0; |
| my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0; |
| my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0; |
| my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0; |
| my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0; |
| my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0; |
| my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0; |
| |
| |
| my $string = "Pass: $status{accepted} Reject: $status{rejected}"; |
| my $string = "Pass: $status{accepted} Reject: $status{rejected}"; |
| $string .= " Refuse: $status{refused}" if $config{do_mid_filter}; |
| $string .= " Refuse: $status{refused}" if $config{do_mid_filter}; |
| $string .= " MD5: $md5hashentries PHL: $phlhashentries PHN: $phnhashentries"; |
| $string .= " MD5: $md5hashentries PHL: $phlhashentries PHN: $phnhashentries"; |
| $string .= " PHR: $phrhashentries FSL: $fslhashentries"; |
| $string .= " PHR: $phrhashentries FSL: $fslhashentries"; |
| $string .= " Arts/sec: $timer{rate} Accept/sec: $timer{accept_rate}" |
| $string .= " Arts/sec: $timer{rate} Accept/sec: $timer{accept_rate}" |
| if $config{timer_info} and $timer{rate}; |
| if $config{timer_info} and $timer{rate}; |
| $string .= " cleanfeed.conf NOT loaded!" if $Local_Conf_Err; |
| $string .= " cleanfeed.conf NOT loaded!" if $Local_Conf_Err; |
| |
| |
| return $string; |
| return $string; |
| } |
| } |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # functions to write the report files |
| # functions to write the report files |
| ############################################################################## |
| ############################################################################## |
| |
| |
| # Write an HTML statfile |
| # Write an HTML statfile |
| sub write_html_stats { |
| sub write_html_stats { |
| if (not open(HTML, ">$config{html_statfile}")) { |
| if (not open(HTML, ">$config{html_statfile}")) { |
| slog('E', "Cannot open $config{html_statfile}: $!"); |
| slog('E', "Cannot open $config{html_statfile}: $!"); |
| return; |
| return; |
| } |
| } |
| |
| |
| print HTML "<html>\n<head>\n" |
| print HTML "<html>\n<head>\n" |
| . "<title>Cleanfeed Status</title>\n" |
| . "<title>Cleanfeed Status</title>\n" |
| . "</head>\n<body>\n\n" |
| . "</head>\n<body>\n\n" |
| . "<p>\n"; |
| . "<p>\n"; |
| #print HTML "<b>Cleanfeed Version:</b> $version ($version_date)<br>\n" |
| #print HTML "<b>Cleanfeed Version:</b> $version ($version_date)<br>\n" |
| # if $version and $version_date; |
| # if $version and $version_date; |
| print HTML "<b>Filter started:</b> " . scalar(localtime $Start_Time) . "<br>\n" |
| print HTML "<b>Filter started:</b> " . scalar(localtime $Start_Time) . "<br>\n" |
| . "<b>Report generated:</b> " . scalar(localtime) . "<br>\n" |
| . "<b>Report generated:</b> " . scalar(localtime) . "<br>\n" |
| . 'Uptime: ' . ($now - $Start_Time) . " seconds\n" |
| . 'Uptime: ' . ($now - $Start_Time) . " seconds\n" |
| . "\n<p>\n" |
| . "\n<p>\n" |
| . "<b>Accepted:</b> $status{accepted}<br>\n" |
| . "<b>Accepted:</b> $status{accepted}<br>\n" |
| . "<b>Rejected:</b> $status{rejected}\n"; |
| . "<b>Rejected:</b> $status{rejected}\n"; |
| print HTML "<br><b>Refused:</b> $status{refused}\n" |
| print HTML "<br><b>Refused:</b> $status{refused}\n" |
| if $config{do_mid_filter}; |
| if $config{do_mid_filter}; |
| |
| |
| if ($config{timer_info} and $timer{rate}) { |
| if ($config{timer_info} and $timer{rate}) { |
| print HTML "\n<p>\n" |
| print HTML "\n<p>\n" |
| . "Period since last report: $timer{interval} seconds<br>\n" |
| . "Period since last report: $timer{interval} seconds<br>\n" |
| . "Articles examined (this period): $timer{rate}/s<br>\n" |
| . "Articles examined (this period): $timer{rate}/s<br>\n" |
| . "Articles accepted (this period): $timer{accept_rate}/s<br>\n" |
| . "Articles accepted (this period): $timer{accept_rate}/s<br>\n" |
| . "Articles examined (entire uptime): $timer{total_rate}/s<br>\n" |
| . "Articles examined (entire uptime): $timer{total_rate}/s<br>\n" |
| . "Articles accepted (entire uptime): $timer{total_accept_rate}/s\n"; |
| . "Articles accepted (entire uptime): $timer{total_accept_rate}/s\n"; |
| } |
| } |
| |
| |
| my $md5hashentries = $MD5history ? $MD5history->count : 0; |
| my $md5hashentries = $MD5history ? $MD5history->count : 0; |
| my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0; |
| my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0; |
| my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0; |
| my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0; |
| my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0; |
| my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0; |
| my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0; |
| my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0; |
| my $superentries = $Suphistory ? $Suphistory->count : 0; |
| my $superentries = $Suphistory ? $Suphistory->count : 0; |
| my $midhistentries = $MIDhistory->count; |
| my $midhistentries = $MIDhistory->count; |
| my $md5count = $MD5history ? $MD5history->overflowed : 0; |
| my $md5count = $MD5history ? $MD5history->overflowed : 0; |
| my $phlcount = $PHLhistory ? $PHLhistory->overflowed : 0; |
| my $phlcount = $PHLhistory ? $PHLhistory->overflowed : 0; |
| my $phncount = $PHNhistory ? $PHNhistory->overflowed : 0; |
| my $phncount = $PHNhistory ? $PHNhistory->overflowed : 0; |
| my $phrcount = $PHRhistory ? $PHRhistory->overflowed : 0; |
| my $phrcount = $PHRhistory ? $PHRhistory->overflowed : 0; |
| my $fslcount = $FSLhistory ? $FSLhistory->overflowed : 0; |
| my $fslcount = $FSLhistory ? $FSLhistory->overflowed : 0; |
| |
| |
| print HTML "\n<p>\n" |
| print HTML "\n<p>\n" |
| . "<b>MD5 entries:</b> $md5hashentries <b>Rejecting:</b> $md5count<br>\n" |
| . "<b>MD5 entries:</b> $md5hashentries <b>Rejecting:</b> $md5count<br>\n" |
| . "<b>PHL entries:</b> $phlhashentries <b>Rejecting:</b> $phlcount<br>\n" |
| . "<b>PHL entries:</b> $phlhashentries <b>Rejecting:</b> $phlcount<br>\n" |
| . "<b>PHN entries:</b> $phnhashentries <b>Rejecting:</b> $phncount<br>\n" |
| . "<b>PHN entries:</b> $phnhashentries <b>Rejecting:</b> $phncount<br>\n" |
| . "<b>PHR entries:</b> $phrhashentries <b>Rejecting:</b> $phrcount<br>\n" |
| . "<b>PHR entries:</b> $phrhashentries <b>Rejecting:</b> $phrcount<br>\n" |
| . "<b>FSL entries:</b> $fslhashentries <b>Rejecting:</b> $fslcount<br>\n" |
| . "<b>FSL entries:</b> $fslhashentries <b>Rejecting:</b> $fslcount<br>\n" |
| . "<b>MID history:</b> $midhistentries\n"; |
| . "<b>MID history:</b> $midhistentries\n"; |
| |
| |
| print HTML "\n<p>\n<blink>cleanfeed.conf <b>NOT</b> loaded!</blink>\n" |
| print HTML "\n<p>\n<blink>cleanfeed.conf <b>NOT</b> loaded!</blink>\n" |
| if $Local_Conf_Err; |
| if $Local_Conf_Err; |
| |
| |
| print HTML "\n<p>\nSupersedes entries: $superentries\n"; |
| print HTML "\n<p>\nSupersedes entries: $superentries\n"; |
| if ($Suphistory) { |
| if ($Suphistory) { |
| print HTML "<ul>\n"; |
| print HTML "<ul>\n"; |
| my $items = $Suphistory->items; |
| my $items = $Suphistory->items; |
| foreach (sort keys %$items) { |
| foreach (sort keys %$items) { |
| print HTML "<li>$_: $items->{$_}\n"; |
| print HTML "<li>$_: $items->{$_}\n"; |
| } |
| } |
| print HTML "</ul>\n"; |
| print HTML "</ul>\n"; |
| } |
| } |
| |
| |
| print HTML "</body></html>\n"; |
| print HTML "</body></html>\n"; |
| close HTML; |
| close HTML; |
| } |
| } |
| |
| |
| # write a crude stat file including accept/reject numbers, |
| # write a crude stat file including accept/reject numbers, |
| # hash sizes, and current configuration |
| # hash sizes, and current configuration |
| sub writestats { |
| sub writestats { |
| my $noreset = $_[0] || 0; |
| my $noreset = $_[0] || 0; |
| $Last_Stats = $now unless $noreset; |
| $Last_Stats = $now unless $noreset; |
| |
| |
| timer_stats() if $config{timer_info}; |
| timer_stats() if $config{timer_info}; |
| |
| |
| write_html_stats() if $config{html_statfile}; |
| write_html_stats() if $config{html_statfile}; |
| |
| write_metrics();
|
| |
| |
| return if not ($config{statfile} or $config{inn_syslog_status}); |
| return if not ($config{statfile} or $config{inn_syslog_status}); |
| |
| |
| my $md5hashentries = $MD5history ? $MD5history->count : 0; |
| my $md5hashentries = $MD5history ? $MD5history->count : 0; |
| my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0; |
| my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0; |
| my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0; |
| my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0; |
| my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0; |
| my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0; |
| my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0; |
| my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0; |
| my $superentries = $Suphistory ? $Suphistory->count : 0; |
| my $superentries = $Suphistory ? $Suphistory->count : 0; |
| my $midhistentries = $MIDhistory->count; |
| my $midhistentries = $MIDhistory->count; |
| |
| |
| if ($config{inn_syslog_status}) { |
| if ($config{inn_syslog_status}) { |
| my $message = 'status: '; |
| my $message = 'status: '; |
| $message .= "accepted $status{accepted} rejected $status{rejected}"; |
| $message .= "accepted $status{accepted} rejected $status{rejected}"; |
| $message .= " refused $status{refused}" if $config{do_mid_filter}; |
| $message .= " refused $status{refused}" if $config{do_mid_filter}; |
| $message .= " md5 $md5hashentries" if $md5hashentries; |
| $message .= " md5 $md5hashentries" if $md5hashentries; |
| $message .= " phl $phlhashentries" if $phlhashentries; |
| $message .= " phl $phlhashentries" if $phlhashentries; |
| $message .= " fsl $fslhashentries" if $fslhashentries; |
| $message .= " fsl $fslhashentries" if $fslhashentries; |
| $message .= " arts/s $timer{rate} accept/s $timer{accept_rate}" |
| $message .= " arts/s $timer{rate} accept/s $timer{accept_rate}" |
| if $config{timer_info} and $timer{rate}; |
| if $config{timer_info} and $timer{rate}; |
| $message .= " WARNING cleanfeed.local NOT loaded" if $Local_Conf_Err; |
| $message .= " WARNING cleanfeed.local NOT loaded" if $Local_Conf_Err; |
| |
| |
| slog('N', $message); |
| slog('N', $message); |
| } |
| } |
| |
| |
| return if not $config{statfile}; |
| return if not $config{statfile}; |
| |
| |
| if (not open FILE, ">$config{statfile}") { |
| if (not open FILE, ">$config{statfile}") { |
| slog('E', "Cannot open $config{statfile}: $!"); |
| slog('E', "Cannot open $config{statfile}: $!"); |
| return; |
| return; |
| } |
| } |
| print FILE "Cleanfeed Version: $version ($version_date)\n" |
| print FILE "Cleanfeed Version: $version ($version_date)\n" |
| if $version and $version_date; |
| if $version and $version_date; |
| print FILE 'Filter started: ' . scalar(localtime $Start_Time) . "\n" |
| print FILE 'Filter started: ' . scalar(localtime $Start_Time) . "\n" |
| . 'Report generated: ' . scalar(localtime) . "\n" |
| . 'Report generated: ' . scalar(localtime) . "\n" |
| . 'Uptime: ' . ($now - $Start_Time) . " seconds\n\n" |
| . 'Uptime: ' . ($now - $Start_Time) . " seconds\n\n" |
| . "Accepted: $status{accepted}\nRejected: $status{rejected}\n"; |
| . "Accepted: $status{accepted}\nRejected: $status{rejected}\n"; |
| print FILE "Refused: $status{refused}\n" if $config{do_mid_filter}; |
| print FILE "Refused: $status{refused}\n" if $config{do_mid_filter}; |
| print FILE "MD5 entries: $md5hashentries\n" |
| print FILE "MD5 entries: $md5hashentries\n" |
| . "PHL entries: $phlhashentries\n" |
| . "PHL entries: $phlhashentries\n" |
| . "PHN entries: $phnhashentries\n" |
| . "PHN entries: $phnhashentries\n" |
| . "PHR entries: $phrhashentries\n" |
| . "PHR entries: $phrhashentries\n" |
| . "FSL entries: $fslhashentries\n" |
| . "FSL entries: $fslhashentries\n" |
| . "MID history: $midhistentries\n\n"; |
| . "MID history: $midhistentries\n\n"; |
| if ($config{timer_info} and $timer{rate}) { |
| if ($config{timer_info} and $timer{rate}) { |
| print FILE "Articles examined per second: $timer{rate}\n"; |
| print FILE "Articles examined per second: $timer{rate}\n"; |
| print FILE "Articles accepted per second: $timer{accept_rate}\n"; |
| print FILE "Articles accepted per second: $timer{accept_rate}\n"; |
| } |
| } |
| |
| |
| print FILE "\ncleanfeed.local NOT loaded! Check file permissions.\n" |
| print FILE "\ncleanfeed.local NOT loaded! Check file permissions.\n" |
| if $Local_Conf_Err; |
| if $Local_Conf_Err; |
| |
| |
| print FILE "\nSupersedes entries: $superentries\n"; |
| print FILE "\nSupersedes entries: $superentries\n"; |
| if ($Suphistory) { |
| if ($Suphistory) { |
| my $items = $Suphistory->items; |
| my $items = $Suphistory->items; |
| foreach (sort keys %$items) { |
| foreach (sort keys %$items) { |
| print FILE " $_: $items->{$_}\n"; |
| print FILE " $_: $items->{$_}\n"; |
| } |
| } |
| } |
| } |
| |
| |
| print FILE "\n\nCurrent configuration:\n\n"; |
| print FILE "\n\nCurrent configuration:\n\n"; |
| foreach my $item (sort keys %config) { |
| foreach my $item (sort keys %config) { |
| print FILE "$item: $config{$item}\n" |
| print FILE "$item: $config{$item}\n" |
| } |
| } |
| |
| |
| # Report on bad_ files read as Hash tables |
| # Report on bad_ files read as Hash tables |
| print FILE "\n\nBad Hashes from Files:\n\n"; |
| print FILE "\n\nBad Hashes from Files:\n\n"; |
| foreach (qw(Bad_Path Bad_Cancel_Path Bad_Adult_Path Bad_Hosts |
| foreach (qw(Bad_Path Bad_Cancel_Path Bad_Adult_Path Bad_Hosts |
| Bad_Hosts_Central)) { |
| Bad_Hosts_Central)) { |
| print FILE "$_:\n"; |
| print FILE "$_:\n"; |
| if (keys(%$_) > 0) { |
| if (keys(%$_) > 0) { |
| for my $hitem ( sort keys %$_ ) { |
| for my $hitem ( sort keys %$_ ) { |
| print FILE "$hitem\n"; |
| print FILE "$hitem\n"; |
| }; |
| }; |
| } else { |
| } else { |
| print FILE "Not Defined\n"; |
| print FILE "Not Defined\n"; |
| }; |
| }; |
| print FILE "\n"; |
| print FILE "\n"; |
| }; |
| }; |
| |
| |
| # Report on bad_ files read as Regexs |
| # Report on bad_ files read as Regexs |
| print FILE "\nBad Regular Expressions from Files:\n\n"; |
| print FILE "\nBad Regular Expressions from Files:\n\n"; |
| foreach (qw(Bad_From Bad_Body Bad_Subject Bad_URL Bad_URL_Central)) { |
| foreach (qw(Bad_From Bad_Body Bad_Subject Bad_URL Bad_URL_Central)) { |
| print FILE "$_:\n$$_\n" if $$_; |
| print FILE "$_:\n$$_\n" if $$_; |
| print FILE "$_:\nNot Defined\n\n" if not $$_; |
| print FILE "$_:\nNot Defined\n\n" if not $$_; |
| }; |
| }; |
| |
| |
| close FILE; |
| close FILE; |
| } |
| } |
| |
| |
| # figure out how many articles per second we're looking at and accepting |
| # figure out how many articles per second we're looking at and accepting |
| # $timer{articles} - how many we've seen since last time |
| # $timer{articles} - how many we've seen since last time |
| # $timer{accepted} - how many we've accepted since last time |
| # $timer{accepted} - how many we've accepted since last time |
| # $timer{time} - time of last check |
| # $timer{time} - time of last check |
| # $timer{interval} - interval time for this check |
| # $timer{interval} - interval time for this check |
| # $timer{rate} - articles checked per second during this interval |
| # $timer{rate} - articles checked per second during this interval |
| # $timer{accept_rate} - articles accepted per second during this interval |
| # $timer{accept_rate} - articles accepted per second during this interval |
| # $timer{total_rate} - articles checked per second since we've been running |
| # $timer{total_rate} - articles checked per second since we've been running |
| # $timer{total_accept_rate} - art. accepted per second since we've been running |
| # $timer{total_accept_rate} - art. accepted per second since we've been running |
| sub timer_stats { |
| sub timer_stats { |
| my $uptime = $now - $Start_Time; |
| my $uptime = $now - $Start_Time; |
| |
| |
| $timer{interval} = $now - $timer{time} || 1; |
| $timer{interval} = $now - $timer{time} || 1; |
| $timer{rate} = (int ($timer{articles} / $timer{interval} * 10)) / 10; |
| $timer{rate} = (int ($timer{articles} / $timer{interval} * 10)) / 10; |
| $timer{accept_rate} = (int ($timer{accepted} / $timer{interval} * 10)) / 10; |
| $timer{accept_rate} = (int ($timer{accepted} / $timer{interval} * 10)) / 10; |
| $timer{total_rate} = (int ($status{articles} / $uptime * 10)) / 10; |
| $timer{total_rate} = (int ($status{articles} / $uptime * 10)) / 10; |
| $timer{total_accept_rate} = (int ($status{accepted} / $uptime * 10)) / 10; |
| $timer{total_accept_rate} = (int ($status{accepted} / $uptime * 10)) / 10; |
| |
| |
| $timer{time} = $now; |
| $timer{time} = $now; |
| $timer{articles} = 0; |
| $timer{articles} = 0; |
| $timer{accepted} = 0; |
| $timer{accepted} = 0; |
| return 1; |
| return 1; |
| } |
| } |
| |
| |
| sub trimhashes { |
| sub trimhashes { |
| $MD5history->trim if $MD5history; |
| $MD5history->trim if $MD5history; |
| $PHLhistory->trim if $PHLhistory; |
| $PHLhistory->trim if $PHLhistory; |
| $PHNhistory->trim if $PHNhistory; |
| $PHNhistory->trim if $PHNhistory; |
| $PHRhistory->trim if $PHRhistory; |
| $PHRhistory->trim if $PHRhistory; |
| $FSLhistory->trim if $FSLhistory; |
| $FSLhistory->trim if $FSLhistory; |
| $Suphistory->trim if $Suphistory; |
| $Suphistory->trim if $Suphistory; |
| $MIDhistory->trim; |
| $MIDhistory->trim; |
| |
| |
| # rotate log if necessary |
| # rotate log if necessary |
| if ($Do_Log == 1) { |
| if ($Do_Log == 1) { |
| if (($config{max_log_size} and -s $Log_File > $config{max_log_size}) |
| if (($config{max_log_size} and -s $Log_File > $config{max_log_size}) |
| or -e $config{rotate_file}) { |
| or -e $config{rotate_file}) { |
| rotate_log(); |
| rotate_log(); |
| unlink $config{rotate_file}; |
| unlink $config{rotate_file}; |
| } |
| } |
| } |
| } |
| |
| |
| $Last_Trim = $now; |
| $Last_Trim = $now; |
| } |
| } |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # debugging functions to save articles |
| # debugging functions to save articles |
| ############################################################################## |
| ############################################################################## |
| sub logart { |
| sub logart { |
| # Output format is controlled at a bit level. |
| # Output format is controlled at a bit level. |
| # Some args are ambiguous, for example if bit 3 (Include Body) is 0 then |
| # Some args are ambiguous, for example if bit 3 (Include Body) is 0 then |
| # bits 4 and 5 are ignored. |
| # bits 4 and 5 are ignored. |
| # 1(1) = Include Post Headers |
| # 1(1) = Include Post Headers |
| # 2(2) = Cleanfeed Internals |
| # 2(2) = Cleanfeed Internals |
| # 3(4) = Include Body |
| # 3(4) = Include Body |
| # 4(8) = Truncate Body (On / Off) |
| # 4(8) = Truncate Body (On / Off) |
| # 5(16) = Processed Body (Lowercase, Truncated, Base64 Decoded) |
| # 5(16) = Processed Body (Lowercase, Truncated, Base64 Decoded) |
| # 6(32) = Just Posting-Host |
| # 6(32) = Just Posting-Host |
| # 7(64) = Just Newsgroups |
| # 7(64) = Just Newsgroups |
| # 8(128) = Just Message-ID |
| # 8(128) = Just Message-ID |
| # 9(256) = Debug Cleanfeed Internals |
| # 9(256) = Debug Cleanfeed Internals |
| |
| |
| my ($file, $info, $format) = @_; |
| my ($file, $info, $format) = @_; |
| # Default format value is 15. |
| # Default format value is 15. |
| $format ||= 15; |
| $format ||= 15; |
| |
| |
| return if not $config{debug_batch_directory}; |
| return if not $config{debug_batch_directory}; |
| checkrotate("$config{debug_batch_directory}/$file"); |
| checkrotate("$config{debug_batch_directory}/$file"); |
| |
| |
| if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) { |
| if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) { |
| slog('E', "Cannot open $file: $!"); |
| slog('E', "Cannot open $file: $!"); |
| return; |
| return; |
| } |
| } |
| |
| |
| # Begin Header cutmark |
| # Begin Header cutmark |
| if ($format & 259) { |
| if ($format & 259) { |
| print LOCAL "From foo\@bar Thu Jan 1 00:00:01 1970\n"; |
| print LOCAL "From foo\@bar Thu Jan 1 00:00:01 1970\n"; |
| }; |
| }; |
| |
| |
| # Cleanfeed Internals |
| # Cleanfeed Internals |
| if ($format & 2) { |
| if ($format & 2) { |
| print LOCAL "INFO: $info\n" if $info; |
| print LOCAL "INFO: $info\n" if $info; |
| print LOCAL "Score: $score\n" if $score; |
| print LOCAL "Score: $score\n" if $score; |
| print LOCAL "Binary: $state{cache_is_binary}\n" if $state{cache_is_binary} |
| print LOCAL "Binary: $state{cache_is_binary}\n" if $state{cache_is_binary} |
| and not $format & 256; |
| and not $format & 256; |
| }; |
| }; |
| |
| |
| # Article Headers |
| # Article Headers |
| if ($format & 1) { |
| if ($format & 1) { |
| foreach (sort keys %hdr) { |
| foreach (sort keys %hdr) { |
| next if $_ eq '__BODY__' or $_ eq '__LINES__'; |
| next if $_ eq '__BODY__' or $_ eq '__LINES__'; |
| print LOCAL "$_: $hdr{$_}\n"; |
| print LOCAL "$_: $hdr{$_}\n"; |
| }; |
| }; |
| } else { |
| } else { |
| # Just Posting-Host |
| # Just Posting-Host |
| print LOCAL "$state{posting_host}\n" if $format & 32; |
| print LOCAL "$state{posting_host}\n" if $format & 32; |
| # Just Newsgroups |
| # Just Newsgroups |
| print LOCAL "$hdr{'Newsgroups'}\n" if $format & 64; |
| print LOCAL "$hdr{'Newsgroups'}\n" if $format & 64; |
| # Just Message-ID |
| # Just Message-ID |
| print LOCAL "$hdr{'Message-ID'}\n" if $format & 128; |
| print LOCAL "$hdr{'Message-ID'}\n" if $format & 128; |
| }; |
| }; |
| |
| |
| # Debug Cleanfeed Internals |
| # Debug Cleanfeed Internals |
| if ($format & 256) { |
| if ($format & 256) { |
| foreach (sort keys %state) { |
| foreach (sort keys %state) { |
| print LOCAL "state{$_}: $state{$_}\n" if $state{$_}; |
| print LOCAL "state{$_}: $state{$_}\n" if $state{$_}; |
| }; |
| }; |
| foreach (sort keys %gr) { |
| foreach (sort keys %gr) { |
| print LOCAL "gr{$_}: $gr{$_}\n" if $gr{$_}; |
| print LOCAL "gr{$_}: $gr{$_}\n" if $gr{$_}; |
| }; |
| }; |
| }; |
| }; |
| |
| |
| # End of headers cutmark |
| # End of headers cutmark |
| if ($format & 259) { |
| if ($format & 259) { |
| print LOCAL "\n"; |
| print LOCAL "\n"; |
| }; |
| }; |
| |
| |
| if ($format & 4) { |
| if ($format & 4) { |
| # Truncated and processed __BODY__ |
| # Truncated and processed __BODY__ |
| if (not $format & 16) { |
| if (not $format & 16) { |
| # Full, unprocessed body |
| # Full, unprocessed body |
| print LOCAL "$hdr{__BODY__}\n" if not $format & 8; |
| print LOCAL "$hdr{__BODY__}\n" if not $format & 8; |
| # Truncated, unprocessed body |
| # Truncated, unprocessed body |
| print LOCAL substr($hdr{__BODY__}, 0, 4000) . "\n\n" |
| print LOCAL substr($hdr{__BODY__}, 0, 4000) . "\n\n" |
| if $format & 8 and not is_binary(); |
| if $format & 8 and not is_binary(); |
| # Make binary even shorter |
| # Make binary even shorter |
| print LOCAL substr($hdr{__BODY__}, 0, 320) . "\n\n" |
| print LOCAL substr($hdr{__BODY__}, 0, 320) . "\n\n" |
| if $format & 8 and is_binary(); |
| if $format & 8 and is_binary(); |
| } else { |
| } else { |
| # processed body (Both truncated or not) |
| # processed body (Both truncated or not) |
| print LOCAL "$body\n\n"; |
| print LOCAL "$body\n\n"; |
| }; |
| }; |
| }; # End of body |
| }; # End of body |
| close LOCAL; |
| close LOCAL; |
| return; |
| return; |
| }; # End of logart |
| }; # End of logart |
| |
| |
| sub saveart { |
| sub saveart { |
| # We recognise two formatting options: |
| # We recognise two formatting options: |
| # 0: Header and body truncated if over 50 lines (Default) |
| # 0: Header and body truncated if over 50 lines (Default) |
| # 1: Header and full body regardless of length |
| # 1: Header and full body regardless of length |
| |
| |
| my ($file, $info, $format) = @_; |
| my ($file, $info, $format) = @_; |
| $format ||= 0; |
| $format ||= 0; |
| |
| |
| return if not $config{debug_batch_directory}; |
| return if not $config{debug_batch_directory}; |
| checkrotate("$config{debug_batch_directory}/$file"); |
| checkrotate("$config{debug_batch_directory}/$file"); |
| |
| |
| if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) { |
| if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) { |
| slog('E', "Cannot open $file: $!"); |
| slog('E', "Cannot open $file: $!"); |
| return; |
| return; |
| } |
| } |
| |
| |
| # Headers common to all remaining report types. |
| # Headers common to all remaining report types. |
| print LOCAL "From foo\@bar Thu Jan 1 00:00:01 1970\n"; |
| print LOCAL "From foo\@bar Thu Jan 1 00:00:01 1970\n"; |
| print LOCAL "INFO: $info\n" if $info; |
| print LOCAL "INFO: $info\n" if $info; |
| print LOCAL "Score: $score\n" if $score; |
| print LOCAL "Score: $score\n" if $score; |
| print LOCAL "Binary: $state{cache_is_binary}\n" |
| print LOCAL "Binary: $state{cache_is_binary}\n" |
| if is_binary(); |
| if is_binary(); |
| foreach (sort keys %hdr) { |
| foreach (sort keys %hdr) { |
| next if $_ eq '__BODY__' or $_ eq '__LINES__'; |
| next if $_ eq '__BODY__' or $_ eq '__LINES__'; |
| print LOCAL "$_: $hdr{$_}\n"; |
| print LOCAL "$_: $hdr{$_}\n"; |
| }; |
| }; |
| |
| |
| if ($format == 1) { # Format 1 - Full message payload. |
| if ($format == 1) { # Format 1 - Full message payload. |
| print LOCAL "$hdr{__BODY__}\n"; |
| print LOCAL "$hdr{__BODY__}\n"; |
| } elsif ($format == 0) { # Format 0 - Cropped payload |
| } elsif ($format == 0) { # Format 0 - Cropped payload |
| print LOCAL substr($hdr{__BODY__}, 0, 3000) . "\n\n"; |
| print LOCAL substr($hdr{__BODY__}, 0, 3000) . "\n\n"; |
| } else { # Undefined format, give up (just headers logged) |
| } else { # Undefined format, give up (just headers logged) |
| print LOCAL "\n"; |
| print LOCAL "\n"; |
| }; |
| }; |
| |
| |
| close LOCAL; |
| close LOCAL; |
| return; |
| return; |
| }; |
| }; |
| |
| |
| # See if batch file is oversized and if so, rotate it |
| # See if batch file is oversized and if so, rotate it |
| sub checkrotate { |
| sub checkrotate { |
| my ($batchfile) = @_; |
| my ($batchfile) = @_; |
| my $num = 1; |
| my $num = 1; |
| |
| |
| return if not $config{debug_batch_size} |
| return if not $config{debug_batch_size} |
| or -s $batchfile < $config{debug_batch_size}; |
| or -s $batchfile < $config{debug_batch_size}; |
| |
| |
| $num += 1 while -e "$batchfile.$num"; # Ensure filename is unique |
| $num += 1 while -e "$batchfile.$num"; # Ensure filename is unique |
| rename $batchfile, "$batchfile.$num"; # Move it out of the way |
| rename $batchfile, "$batchfile.$num"; # Move it out of the way |
| } |
| } |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # internal state dump and restore |
| # internal state dump and restore |
| ############################################################################## |
| ############################################################################## |
| sub dump_emp { |
| sub dump_emp { |
| return if not $config{emp_dump_file}; |
| return if not $config{emp_dump_file}; |
| |
| |
| if (not open(DUMP, ">$config{emp_dump_file}")) { |
| if (not open(DUMP, ">$config{emp_dump_file}")) { |
| slog('E', "EMP database could not be dumped: $!"); |
| slog('E', "EMP database could not be dumped: $!"); |
| return; |
| return; |
| } |
| } |
| |
| |
| $MD5history->dump('MD5history', \*DUMP) if $MD5history; |
| $MD5history->dump('MD5history', \*DUMP) if $MD5history; |
| $PHLhistory->dump('PHLhistory', \*DUMP) if $PHLhistory; |
| $PHLhistory->dump('PHLhistory', \*DUMP) if $PHLhistory; |
| $PHNhistory->dump('PHNhistory', \*DUMP) if $PHNhistory; |
| $PHNhistory->dump('PHNhistory', \*DUMP) if $PHNhistory; |
| $PHRhistory->dump('PHRhistory', \*DUMP) if $PHRhistory; |
| $PHRhistory->dump('PHRhistory', \*DUMP) if $PHRhistory; |
| $FSLhistory->dump('FSLhistory', \*DUMP) if $FSLhistory; |
| $FSLhistory->dump('FSLhistory', \*DUMP) if $FSLhistory; |
| |
| |
| close DUMP; |
| close DUMP; |
| |
| |
| slog('N', 'Saved EMP database.'); |
| slog('N', 'Saved EMP database.'); |
| } |
| } |
| |
| |
| sub restore_emp { |
| sub restore_emp { |
| return if not $config{emp_dump_file} or not -r $config{emp_dump_file}; |
| return if not $config{emp_dump_file} or not -r $config{emp_dump_file}; |
| |
| |
| do $config{emp_dump_file}; |
| do $config{emp_dump_file}; |
| |
| |
| # delete the data of checks which have been disabled since the last dump |
| # delete the data of checks which have been disabled since the last dump |
| undef $MD5history if not $config{do_md5}; |
| undef $MD5history if not $config{do_md5}; |
| undef $PHLhistory if not $config{do_phl}; |
| undef $PHLhistory if not $config{do_phl}; |
| undef $PHNhistory if not $config{do_phn}; |
| undef $PHNhistory if not $config{do_phn}; |
| undef $PHRhistory if not $config{do_phr}; |
| undef $PHRhistory if not $config{do_phr}; |
| undef $FSLhistory if not $config{do_fsl}; |
| undef $FSLhistory if not $config{do_fsl}; |
| |
| |
| # We can't syslog at startup because INN doesn't provide the callbacks |
| # We can't syslog at startup because INN doesn't provide the callbacks |
| # in time |
| # in time |
| slog('N', 'Restored EMP database.') if not defined $Start_Time; |
| slog('N', 'Restored EMP database.') if not defined $Start_Time; |
| } |
| } |
| |
| |
| sub slog { |
| sub slog { |
| return if not defined &INN::syslog; |
| return if not defined &INN::syslog; |
| INN::syslog(@_); |
| INN::syslog(@_); |
| } |
| } |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # parse the data files |
| # parse the data files |
| ############################################################################## |
| ############################################################################## |
| sub read_hashes { |
| sub read_hashes { |
| read_hash('bad_paths', \%Bad_Path); |
| read_hash('bad_paths', \%Bad_Path); |
| read_hash('bad_cancel_paths', \%Bad_Cancel_Path); |
| read_hash('bad_cancel_paths', \%Bad_Cancel_Path); |
| read_hash('bad_adult_paths', \%Bad_Adult_Path); |
| read_hash('bad_adult_paths', \%Bad_Adult_Path); |
| read_hash('bad_hosts', \%Bad_Hosts); |
| read_hash('bad_hosts', \%Bad_Hosts); |
| read_hash('bad_hosts_central', \%Bad_Hosts_Central); |
| read_hash('bad_hosts_central', \%Bad_Hosts_Central); |
|
|
| load_external_regex('trusted_paths', 'Trusted_Paths', 'Trusted_Paths_RE', sub { qr/(?:$_[0])/i });
|
| # Bad From Headers |
| load_external_regex('trusted_hosts', 'Trusted_Hosts', 'Trusted_Hosts_RE', sub { qr/(?:$_[0])/i }); |
| read_regex('bad_from', \$Bad_From); |
| load_external_regex('trusted_from', 'Trusted_From', 'Trusted_From_RE', sub { qr/(?:$_[0])/i }); |
| $Bad_From_RE = qr/($Bad_From)/i if $Bad_From; |
| load_external_regex('trusted_message_ids', 'Trusted_Message_IDs', 'Trusted_Message_IDs_RE', sub { qr/(?:$_[0])/i }); |
| # Bad Subject Headers
|
|
|
| read_regex('bad_subject', \$Bad_Subject); |
| # Compile external regex files defensively. A malformed expression must not |
| $Bad_Subject_RE = qr/($Bad_Subject)/i if $Bad_Subject; |
| # terminate innd or replace the last known-good compiled expression. |
| # Bad Body |
| load_external_regex('bad_from', 'Bad_From', 'Bad_From_RE', sub { qr/($_[0])/i }); |
| read_regex('bad_body', \$Bad_Body); |
| load_external_regex('bad_subject', 'Bad_Subject', 'Bad_Subject_RE', sub { qr/($_[0])/i }); |
| $Bad_Body_RE = qr/($Bad_Body)/ if $Bad_Body; |
| load_external_regex('bad_body', 'Bad_Body', 'Bad_Body_RE', sub { qr/($_[0])/ }); |
| # Bad URL's in Body |
| load_external_regex('bad_url', 'Bad_URL', 'Bad_URL_RE', sub { qr/$HTTP\S*($_[0])/i }); |
| read_regex('bad_url', \$Bad_URL); |
| load_external_regex('bad_url_central', 'Bad_URL_Central', 'Bad_URL_Central_RE', sub { qr/$HTTP\S*($_[0])/i }); |
| $Bad_URL_RE = qr/$HTTP\S*($Bad_URL)/i if $Bad_URL; |
| update_bad_file_mtimes(); |
| # Bad URL's in Body (Central Resource) |
| update_config_fingerprint() if $config{config_fingerprint_enabled}; |
| read_regex('bad_url_central', \$Bad_URL_Central);
|
| |
| $Bad_URL_Central_RE = qr/$HTTP\S*($Bad_URL_Central)/i if $Bad_URL_Central;
|
| |
| }; |
| }; |
| |
|
|
| |
| sub load_external_regex {
|
| |
| my ($file, $text_name, $compiled_name, $builder) = @_;
|
| |
| no strict 'refs';
|
| |
|
|
| |
| my $candidate = '';
|
| |
| read_regex($file, \$candidate);
|
| |
| if ($candidate eq '') {
|
| |
| ${$text_name} = '';
|
| |
| ${$compiled_name} = undef;
|
| |
| return 1;
|
| |
| }
|
| |
|
|
| |
| if (!external_regex_safe($candidate, $file)) {
|
| |
| slog('E', "Ignoring unsafe $file regex; keeping previous compiled value");
|
| |
| return 0;
|
| |
| }
|
| |
| my $compiled = eval { $builder->($candidate) };
|
| |
| if (not $compiled or $@) {
|
| |
| slog('E', "Ignoring invalid $file regex; keeping previous compiled value: $@");
|
| |
| return 0;
|
| |
| }
|
| |
| ${$text_name} = $candidate;
|
| |
| ${$compiled_name} = $compiled;
|
| |
| return 1;
|
| |
| }
|
| |
| |
| sub read_hash { |
| sub read_hash { |
| my ($file, $hash) = @_; |
| my ($file, $hash) = @_; |
| |
| |
| my @list; |
| my @list; |
| read_file("$config_dir/$file", \@list); |
| read_file("$config_dir/$file", \@list); |
| %$hash = map { $_ => 1 } @list; |
| %$hash = map { $_ => 1 } @list; |
| } |
| } |
| |
| |
| sub read_regex { |
| sub read_regex { |
| my ($file, $regex) = @_; |
| my ($file, $regex) = @_; |
| |
| |
| my @list; |
| my @list; |
| read_file("$config_dir/$file", \@list); |
| read_file("$config_dir/$file", \@list); |
| $$regex = join('|', @list); |
| $$regex = join('|', @list); |
| $$regex =~ s#\|\|#|#g; |
| $$regex =~ s#\|\|#|#g; |
| } |
| } |
| |
| |
| sub read_file { |
| sub read_file { |
| my ($file, $array) = @_; |
| my ($file, $array) = @_; |
| |
| |
| return if not -e $file; |
| return if not -e $file; |
| if (not open(FILE, $file)) { |
| if (not open(FILE, $file)) { |
| slog('E', "Cannot open $file: $!"); |
| slog('E', "Cannot open $file: $!"); |
| return; |
| return; |
| } |
| } |
| while (<FILE>) { |
| while (<FILE>) { |
| s/#.*//; |
| s/#.*//; |
| s/^$hws*(.*?)$hws*$/$1/; |
| s/^$hws*(.*?)$hws*$/$1/; |
| next if /^$/; |
| next if /^$/; |
| if (/\s/) { |
| if (/\s/) { |
| push @$array, split; |
| push @$array, split; |
| } else { |
| } else { |
| push @$array, $_; |
| push @$array, $_; |
| } |
| } |
| } |
| } |
| close FILE; |
| close FILE; |
| } |
| } |
| |
| |
| sub messageid_re { |
| sub messageid_re { |
| my $mdtext = '[\x21-\x3d\x3f\x41-\x5a\x5e-\x7e]+'; |
| my $mdtext = '[\x21-\x3d\x3f\x41-\x5a\x5e-\x7e]+'; |
| my $nofoldliteral = '\[' . $mdtext . '\]'; |
| my $nofoldliteral = '\[' . $mdtext . '\]'; |
| my $idright = '(?:' . $mdtext . '|' . $nofoldliteral . ')'; |
| my $idright = '(?:' . $mdtext . '|' . $nofoldliteral . ')'; |
| my $idleft = $mdtext; |
| my $idleft = $mdtext; |
| my $msgidcore = $idleft . '\@' . $idright; |
| my $msgidcore = $idleft . '\@' . $idright; |
| my $msgid = '<' . $msgidcore . '>'; |
| my $msgid = '<' . $msgidcore . '>'; |
| my $messageid = '^[\x20\x09]*' . $msgid . '[\x20\x09]*$'; |
| my $messageid = '^[\x20\x09]*' . $msgid . '[\x20\x09]*$'; |
| my $messageidre = qr/$messageid/; |
| my $messageidre = qr/$messageid/; |
| return $messageidre; |
| return $messageidre; |
| }; |
| }; |
| |
| |
| print $fullURL if 0; # lint food |
| print $fullURL if 0; # lint food |
| |
| |
| ############################################################################## |
| ############################################################################## |
| # EMP filters |
| # EMP filters |
| ############################################################################## |
| ############################################################################## |
| package Cleanfeed::RateLimit; |
| package Cleanfeed::RateLimit; |
| |
| |
| use strict; |
| use strict; |
| |
| |
| sub new { |
| sub new { |
| my $class = shift; |
| my $class = shift; |
| my $self = { |
| my $self = { |
| ratecutoff => 4, # reject if this many copies are in the history |
| ratecutoff => 4, # reject if this many copies are in the history |
| rateceiling => 85, # only count this high |
| rateceiling => 85, # only count this high |
| ratebaseinterval => 7200, # how long to wait before decrementing count |
| ratebaseinterval => 7200, # how long to wait before decrementing count |
| history => { }, |
| history => { }, |
| }; |
| }; |
| bless $self, $class; |
| bless $self, $class; |
| return $self; |
| return $self; |
| } |
| } |
| |
| |
| sub init { |
| sub init { |
| my ($self, $rco, $rc, $rb) = @_; |
| my ($self, $rco, $rc, $rb) = @_; |
| $self->{ratecutoff} = $rco if defined $rco; |
| $self->{ratecutoff} = $rco if defined $rco; |
| $self->{rateceiling} = $rc if defined $rc; |
| $self->{rateceiling} = $rc if defined $rc; |
| $self->{ratebaseinterval} = $rb if defined $rb; |
| $self->{ratebaseinterval} = $rb if defined $rb; |
| |
| |
| $self->{dectable} = $self->make_curve_table($self->{rateceiling} + 1, |
| $self->{dectable} = $self->make_curve_table($self->{rateceiling} + 1, |
| $self->{ratebaseinterval}); |
| $self->{ratebaseinterval}); |
| } |
| } |
| |
| |
| # return true if over ratecutoff |
| # return true if over ratecutoff |
| sub add { |
| sub add { |
| my ($self, $elem) = @_; |
| my ($self, $elem) = @_; |
| |
| |
| $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem}; |
| $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem}; |
| $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ]; |
| $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ]; |
| $self->{history}->{$elem}[0] = $self->{rateceiling} |
| $self->{history}->{$elem}[0] = $self->{rateceiling} |
| if $self->{history}->{$elem}[0] > $self->{rateceiling}; |
| if $self->{history}->{$elem}[0] > $self->{rateceiling}; |
| |
| |
| return 1 if $self->{history}->{$elem}[0] > $self->{ratecutoff}; |
| return 1 if $self->{history}->{$elem}[0] > $self->{ratecutoff}; |
| return 0; |
| return 0; |
| } |
| } |
| |
| |
| sub add2 { |
| sub add2 { |
| my ($self, $elem, $ratecutoff) = @_; |
| my ($self, $elem, $ratecutoff) = @_; |
| |
| |
| $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem}; |
| $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem}; |
| $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ]; |
| $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ]; |
| $self->{history}->{$elem}[0] = $self->{rateceiling} |
| $self->{history}->{$elem}[0] = $self->{rateceiling} |
| if $self->{history}->{$elem}[0] > $self->{rateceiling}; |
| if $self->{history}->{$elem}[0] > $self->{rateceiling}; |
| |
| |
| return 1 if $self->{history}->{$elem}[0] > $ratecutoff; |
| return 1 if $self->{history}->{$elem}[0] > $ratecutoff; |
| return 0; |
| return 0; |
| } |
| } |
| |
| |
| sub trim { |
| sub trim { |
| my ($self) = @_; |
| my ($self) = @_; |
| my $now = time; |
| my $now = time; |
| |
| |
| my @del; |
| my @del; |
| while (my ($id, $val) = each %{$self->{history}}) { |
| while (my ($id, $val) = each %{$self->{history}}) { |
| if ($now - $val->[1] > $self->{dectable}->[$val->[0]]) { |
| if ($now - $val->[1] > $self->{dectable}->[$val->[0]]) { |
| $self->{history}->{$id}[0]--; |
| $self->{history}->{$id}[0]--; |
| $self->{history}->{$id}[1] = $now; |
| $self->{history}->{$id}[1] = $now; |
| } |
| } |
| push @del, $id if $self->{history}->{$id}[0] < 1; |
| push @del, $id if $self->{history}->{$id}[0] < 1; |
| } |
| } |
| delete @{$self->{history}}{@del}; |
| delete @{$self->{history}}{@del}; |
| } |
| } |
| |
| |
| sub count { |
| sub count { |
| my ($self) = @_; |
| my ($self) = @_; |
| return scalar keys %{$self->{history}}; |
| return scalar keys %{$self->{history}}; |
| } |
| } |
| |
| |
| sub overflowed { |
| sub overflowed { |
| my ($self) = @_; |
| my ($self) = @_; |
| my $count = 0; |
| my $count = 0; |
| |
| |
| foreach (keys %{$self->{history}}) { |
| foreach (keys %{$self->{history}}) { |
| $count++ if $self->{history}->{$_}[0] > $self->{ratecutoff}; |
| $count++ if $self->{history}->{$_}[0] > $self->{ratecutoff}; |
| } |
| } |
| return $count; |
| return $count; |
| } |
| } |
| |
| |
| sub dump { |
| sub dump { |
| my ($self, $name, $fd) = @_; |
| my ($self, $name, $fd) = @_; |
| |
| |
| my $dd = Data::Dumper->new([ $self->{history} ], [ $name.'->{history}' ]); |
| my $dd = Data::Dumper->new([ $self->{history} ], [ $name.'->{history}' ]); |
| $dd->Indent(1); |
| $dd->Indent(1); |
| print $fd $dd->Dumpxs; |
| print $fd $dd->Dumpxs; |
| } |
| } |
| |
| |
| sub items { |
| sub items { |
| my ($self) = @_; |
| my ($self) = @_; |
| |
| |
| return { |
| return { |
| map { $_ => @{$self->{history}->{$_}}[0] } keys %{$self->{history}} |
| map { $_ => @{$self->{history}->{$_}}[0] } keys %{$self->{history}} |
| }; |
| }; |
| } |
| } |
| |
| |
| # Create a lookup table of values on a descending curve |
| # Create a lookup table of values on a descending curve |
| sub make_curve_table { |
| sub make_curve_table { |
| my ($self, $xmax, $ymax) = @_; |
| my ($self, $xmax, $ymax) = @_; |
| my @values; |
| my @values; |
| |
| |
| for (1..$xmax) { |
| for (1..$xmax) { |
| $values[$_] = $ymax - int((($_ / $xmax) ** 2) * $ymax); |
| $values[$_] = $ymax - int((($_ / $xmax) ** 2) * $ymax); |
| } |
| } |
| return \@values; |
| return \@values; |
| } |
| } |
| |
| |
| ############################################################################## |
| ############################################################################## |
| package Cleanfeed::Queue; |
| package Cleanfeed::Queue; |
| |
| |
| sub new { |
| sub new { |
| my $class = shift; |
| my $class = shift; |
| my $self = { |
| my $self = { |
| maxlife => 3600, |
| maxlife => 3600, |
| history => { }, |
| history => { }, |
| }; |
| }; |
| bless $self, $class; |
| bless $self, $class; |
| return $self; |
| return $self; |
| } |
| } |
| |
| |
| sub add { |
| sub add { |
| my ($self, $elem) = @_; |
| my ($self, $elem) = @_; |
| |
| |
| $self->{history}->{$elem} = time; |
| $self->{history}->{$elem} = time; |
| } |
| } |
| |
| |
| sub check { |
| sub check { |
| my ($self, $elem) = @_; |
| my ($self, $elem) = @_; |
| |
| |
| return 1 if exists $self->{history}->{$elem}; |
| return 1 if exists $self->{history}->{$elem}; |
| return 0; |
| return 0; |
| } |
| } |
| |
| |
| sub count { |
| sub count { |
| return scalar keys %{$_[0]->{history}}; |
| return scalar keys %{$_[0]->{history}}; |
| } |
| } |
| |
| |
| sub maxlife { |
| sub maxlife { |
| my $self = $_[0]; |
| my $self = $_[0]; |
| $self->{maxlife} = $_[1] if $_[1]; |
| $self->{maxlife} = $_[1] if $_[1]; |
| $self->{maxlife} = $_[1]; |
| $self->{maxlife} = $_[1]; |
| } |
| } |
| |
| |
| sub trim { |
| sub trim { |
| my ($self) = @_; |
| my ($self) = @_; |
| my $now = time; |
| my $now = time; |
| |
| |
| my @del; |
| my @del; |
| while (my ($id, $val) = each %{$self->{history}}) { |
| while (my ($id, $val) = each %{$self->{history}}) { |
| push @del, $id if $now - $val > $self->{maxlife}; |
| push @del, $id if $now - $val > $self->{maxlife}; |
| } |
| } |
| delete @{$self->{history}}{@del}; |
| delete @{$self->{history}}{@del}; |
| } |
| } |
| |
| |
| 1; |
| 1; |
| |
| |