########################################################################################################################## # $Id: 93_Log2Syslog.pm 23076 2020-11-02 18:50:38Z DS_Starter $ ########################################################################################################################## # 93_Log2Syslog.pm # # (c) 2017-2021 by Heiko Maaz # e-mail: Heiko dot Maaz at t-online dot de # # This script is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # # The module is inspired by 92_rsyslog.pm (betateilchen) # # Implements the Syslog Protocol according to RFCs: # RFC 5424 https://tools.ietf.org/html/rfc5424 # RFC 3164 https://tools.ietf.org/html/rfc3164 and # TLS Transport according to RFC 5425 https://tools.ietf.org/pdf/rfc5425.pdf # Date and Time according to RFC 3339 https://tools.ietf.org/html/rfc3339 # RFC 6587 Transmission of Syslog Messages over TCP # ########################################################################################################################## package FHEM::Log2Syslog; ## no critic 'package' use strict; use warnings; use TcpServerUtils; use POSIX; use Scalar::Util qw(looks_like_number); use Time::HiRes qw(gettimeofday); use Encode qw(encode_utf8 decode_utf8); eval "use IO::Socket::INET;1" or my $MissModulSocket = "IO::Socket::INET"; ## no critic 'eval' eval "use Net::Domain qw(hostname hostfqdn hostdomain domainname);1" or my $MissModulNDom = "Net::Domain"; ## no critic 'eval' eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' use GPUtils qw(GP_Import GP_Export); # Run before module compilation BEGIN { # Import from main:: GP_Import( qw( attr AttrVal currlogfile CommandDelete defs deviceEvents devspec2array DoTrigger fhemTimeLocal fhemTzOffset init_done InternalTimer IsDisabled logInform logopened Log3 modules notifyRegexpChanged OpenLogfile perlSyntaxCheck readyfnlist readingFnAttributes RemoveInternalTimer readingsBeginUpdate readingsBulkUpdate readingsEndUpdate readingsSingleUpdate ReadingsVal ResolveDateWildcards selectlist sortTopicNum TcpServer_Open TcpServer_Accept TcpServer_Close TcpServer_SetSSL TimeNow ) ); # Export to main context with different name # my $pkg = caller(0); # my $main = $pkg; # $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/g; # foreach (@_) { # *{ $main . $_ } = *{ $pkg . '::' . $_ }; # } GP_Export( qw( Initialize ) ); } # Versions History intern: my %vNotesIntern = ( "5.12.4" => "27.02.2021 don't split data by CRLF if EOF is used (in getIfData) ", "5.12.3" => "02.11.2020 avoid do Logfile archiving which was executed in seldom (unknown) cases ", "5.12.2" => "15.05.2020 permit content of 'exclErrCond' to fhemLog strings ", "5.12.1" => "12.05.2020 add dev to check regex of 'exclErrCond' ", "5.12.0" => "16.04.2020 improve IETF octet count again, internal code changes for PBP ", "5.11.0" => "14.04.2020 switch to packages, improve IETF octet count ", "5.10.3" => "11.04.2020 new reading 'Parse_Err_LastData', change octet count read ", "5.10.2" => "08.04.2020 code changes to stabilize send process, minor fixes ", "5.10.1" => "06.04.2020 support time-secfrac of RFC 3339, minor fix ", "5.10.0" => "04.04.2020 new attribute 'timeSpec', send and parse messages according to UTC or Local time, some minor fixes (e.g. for Octet Count) ", "5.9.0" => "01.04.2020 Parser UniFi Controller Syslog (BSD Format) and Netconsole messages, more code review (e.g. remove prototypes) ", "5.8.3" => "31.03.2020 fix warning uninitialized value \$pp in pattern match (m//) at line 465, Forum: topic,75426.msg1036553.html#msg1036553, some code review ", "5.8.2" => "28.07.2019 fix warning uninitialized value in numeric ge (>=) at line 662 ", "5.8.1" => "23.07.2019 attribute waitForEOF rename to useEOF, useEOF also for type sender ", "5.8.0" => "20.07.2019 attribute waitForEOF, solution for Forum: https://forum.fhem.de/index.php/topic,75426.msg958836.html#msg958836 ", "5.7.0" => "20.07.2019 change logging and chomp received data, use raw parse format if automatic mode don't detect a valid format, ". "change getifdata tcp stack error handling (if sysread undef)", "5.6.5" => "19.07.2019 bugfix parse BSD if ID (TAG) is used, function DbLog_splitFn -> DbLogSplit, new attribute useParsefilter ", "5.6.4" => "19.07.2019 minor changes and fixes (max. lenth read to 16384, code && logging) ", "5.6.3" => "18.07.2019 fix state reading if changed disabled attribute ", "5.6.2" => "17.07.2019 Forum: https://forum.fhem.de/index.php/topic,75426.msg958836.html#msg958836 first try", "5.6.1" => "24.03.2019 prevent module from deactivation in case of unavailable Meta.pm ", "5.6.0" => "23.03.2019 attribute exclErrCond to exclude events from rating as \"error\" ", "5.5.0" => "18.03.2019 prepare for Meta.pm ", "5.4.0" => "17.03.2019 new feature parseProfile = Automatic ", "5.3.2" => "08.02.2019 fix version numbering ", "5.3.1" => "21.10.2018 get of FQDN changed ", "5.3.0" => "16.10.2018 attribute sslCertPrefix added (Forum:#92030), module hints & release info order switched ", "5.2.1" => "08.10.2018 setpayload of BSD-format changed, commandref revised ", "5.2.0" => "02.10.2018 added direct help for attributes", "5.1.0" => "01.10.2018 new get versionNotes command", "5.0.1" => "27.09.2018 closeSocket if write error:.* , delete readings code changed", "5.0.0" => "26.09.2018 TCP-Server in Collector-mode, HIPCACHE added, PROFILE as Internal, Parse_Err_No as reading ". "octetCount attribute, TCP-SSL-support, set 'reopen' command, code fixes", "4.8.5" => "20.08.2018 BSD/parseFn parsing changed, BSD setpayload changed, new variable \$IGNORE in parseFn", "4.8.4" => "15.08.2018 BSD parsing changed", "4.8.3" => "14.08.2018 BSD setpayload changed, BSD parsing changed, Internal MYFQDN", "4.8.2" => "13.08.2018 rename makeMsgEvent to makeEvent", "4.8.1" => "12.08.2018 IETF-Syslog without VERSION changed, Log verbose 1 to 2 changed in parsePayload", "4.8.0" => "12.08.2018 enhanced IETF Parser to match logs without version", "4.7.0" => "10.08.2018 Parser for TPLink", "4.6.1" => "10.08.2018 some perl warnings, changed IETF Parser", "4.6.0" => "08.08.2018 set sendTestMessage added, Attribute 'contDelimiter', 'respectSeverity'", "4.5.1" => "07.08.2018 BSD Regex changed, setpayload of BSD changed", "4.5.0" => "06.08.2018 Regex capture groups used in parsePayload to set variables, parsing of BSD changed ". "Attribute 'makeMsgEvent' added", "4.4.0" => "04.08.2018 Attribute 'outputFields' added", "4.3.0" => "03.08.2018 Attribute 'parseFn' added", "4.2.0" => "03.08.2018 evaluate sender peer ip-address/hostname, use it as reading in event generation", "4.1.0" => "02.08.2018 state event generation changed", "4.0.0" => "30.07.2018 server mode (Collector)", "3.2.1" => "04.05.2018 fix compatibility with newer IO::Socket::SSL on debian 9, attr ssldebug for ". "debugging SSL messages", "3.2.0" => "22.11.2017 add NOTIFYDEV if possible", "3.1.0" => "28.08.2017 get-function added, commandref revised, \$readingFnAttributes deleted", "3.0.0" => "27.08.2017 change attr type to protocol, ready to check in", "2.6.0" => "26.08.2017 more than one Log2Syslog device can be created", "2.5.2" => "26.08.2018 fix in splitting timestamp, change calcTrate using internaltimer with attr ". "rateCalcRerun, function closeSocket", "2.5.1" => "24.08.2017 some fixes", "2.5.0" => "23.08.2017 TLS encryption available, new readings, \$readingFnAttributes", "2.4.1" => "21.08.2017 changes in charFilter, change PROCID to \$hash->{SEQNO} ". "switch to non-blocking in subs event/fhemLog", "2.4.0" => "20.08.2017 new sub Log3slog for entries in local fhemlog only -> verbose support", "2.3.1" => "19.08.2017 commandref revised ", "2.3.0" => "18.08.2017 new parameter 'ident' in DEF, sub setidex, charFilter", "2.2.0" => "17.08.2017 set BSD data length, set only acceptable characters (USASCII) in payload ". "commandref revised ", "2.1.0" => "17.08.2017 sub openSocket created", "2.0.0" => "16.08.2017 create syslog without SYS::SYSLOG", "1.1.1" => "13.08.2017 registrate fhemLog to %loginform in case of sending fhem-log ". "attribute timeout, commandref revised", "1.1.0" => "26.07.2017 add regex search to sub fhemLog", "1.0.0" => "25.07.2017 initial version" ); # Versions History extern: my %vNotesExtern = ( "5.10.0" => "04.04.2020 The new attribute 'timeSpec' can be set to send and receive/parse messages according to UTC or Local time format. ". "Please refer to Date and Time on the Internet: Timestamps for further information ", "5.9.0" => "01.04.2020 The new option \"UniFi\" of attribute \"parseProfil\" provedes a new Parser for UniFi Controller Syslog messages ". "and Netconsole messages. It was tested with UniFi AP-AC-Lite but should run with all Unifi products. ", "5.8.1" => "23.07.2019 New attribute \"useParsefilter\" to remove other characters than ASCII from payload before parse it. ". "New attribute \"useEOF\" to parse not till the sender was sending an EOF signal (Collector), or in ". "case of model Sender, after transmission an EOF signal is send. A bugfix for ". "parsing BSD if the ID (TAG) is used was implemented. Minor other fixes and changes. ", "5.6.0" => "23.03.2019 New attribute \"exclErrCond\" to exclude events from rating as \"Error\" even though the ". "event contains the text \"Error\". ", "5.4.0" => "17.03.2019 New feature parseProfile = Automatic. The module may detect the message format BSD or IETF automatically in server mode ", "5.3.2" => "08.02.2019 fix version numbering ", "5.3.0" => "16.10.2018 attribute sslCertPrefix added to support multiple SSL-keys (Forum:#92030)", "5.2.1" => "08.10.2018 Send format of BSD changed. The TAG-field was changed to \"IDENT[PID]: \" ", "5.2.0" => "02.10.2018 direct help for attributes added", "5.1.0" => "29.09.2018 new get <name> versionNotes command ", "5.0.1" => "27.09.2018 automatic reconnect to syslog-server in case of write error ", "5.0.0" => "26.09.2018 Some changes:
  • TCP Server mode is possible now for Collector devices<\li>
  • the used parse-profile is shown as Internal<\li>
  • Parse_Err_No counts faulty persings since start<\li>
  • new octetCount attribute switches the syslog framing method (see also RFC6587 Transmission of Syslog Messages over TCP)<\li>
  • TCP SSL-support<\li>
  • new set 'reopen' command to reconnect a broken connection<\li>
  • some code fixes ", "4.8.5" => "20.08.2018 BSD/parseFn parse changed, BSD setpayload changed, new variable \$IGNORE in parseFn ", "4.8.4" => "15.08.2018 BSD parse changed again ", "4.8.3" => "14.08.2018 BSD setpayload changed, BSD parse changed, new Internal MYFQDN ", "4.8.2" => "13.08.2018 rename makeMsgEvent to makeEvent ", "4.8.1" => "12.08.2018 IETF-Syslog without VERSION changed, Log verbose 1 to 2 changed in parsePayload ", "4.8.0" => "12.08.2018 enhanced IETF Parser to match logs without version ", "4.7.0" => "10.08.2018 Parser for TPLink added ", "4.6.1" => "10.08.2018 fix some perl warnings, changed IETF Parser ", "4.6.0" => "08.08.2018 set sendTestMessage added, new attributes 'contDelimiter', 'respectSeverity' ", "4.5.1" => "07.08.2018 BSD Regex changed, setpayload of BSD changed ", "4.5.0" => "06.08.2018 parsing of BSD changed, attribute 'makeMsgEvent' added ", "4.4.0" => "04.08.2018 Attribute 'outputFields' added ", "4.3.0" => "03.08.2018 Attribute 'parseFn' added ", "4.2.0" => "03.08.2018 evaluate sender peer ip-address/hostname and use it as reading in event generation ", "4.1.0" => "02.08.2018 state event generation changed ", "4.0.0" => "30.07.2018 Server mode (Collector) implemented ", "3.2.1" => "04.05.2018 fix compatibility with newer IO::Socket::SSL on debian 9, attribute ssldebug for debugging SSL messages ", "3.2.0" => "22.11.2017 add NOTIFYDEV if possible ", "3.1.0" => "28.08.2017 get-function added, commandref revised ", "3.0.0" => "27.08.2017 change attr type to protocol, ready to first check in ", "2.6.0" => "26.08.2017 more than one Log2Syslog device can be created ", "2.5.2" => "26.08.2018 attribute rateCalcRerun ", "2.5.1" => "24.08.2017 some bugfixes ", "2.5.0" => "23.08.2017 TLS encryption available to Sender ", "2.4.1" => "21.08.2017 change PROCID to \$hash->{SEQNO}, switch to non-blocking in subs event/fhemlog ", "2.4.0" => "20.08.2017 new sub for entries in local fhemlog only including verbose support ", "2.3.1" => "19.08.2017 commandref revised ", "2.3.0" => "18.08.2017 new parameter 'ident' in Define to indentify sylog source ", "2.2.0" => "17.08.2017 set BSD data length, set only acceptable characters (USASCII) in payload ", "2.0.0" => "16.08.2017 create syslog without perl module SYS::SYSLOG ", "1.1.0" => "26.07.2017 add regex search to sub fhemLog ", "1.0.0" => "25.07.2017 initial version " ); # Mappinghash BSD-Formatierung Monat my %Log2Syslog_BSDMonth = ( "01" => "Jan", "02" => "Feb", "03" => "Mar", "04" => "Apr", "05" => "May", "06" => "Jun", "07" => "Jul", "08" => "Aug", "09" => "Sep", "10" => "Oct", "11" => "Nov", "12" => "Dec", "Jan" => "01", "Feb" => "02", "Mar" => "03", "Apr" => "04", "May" => "05", "Jun" => "06", "Jul" => "07", "Aug" => "08", "Sep" => "09", "Oct" => "10", "Nov" => "11", "Dec" => "12" ); # Mappinghash Severity my %Log2Syslog_Severity = ( "0" => "Emergency", "1" => "Alert", "2" => "Critical", "3" => "Error", "4" => "Warning", "5" => "Notice", "6" => "Informational", "7" => "Debug", "Emergency" => "0", "Alert" => "1", "Critical" => "2", "Error" => "3", "Warning" => "4", "Notice" => "5", "Informational" => "6", "Debug" => "7" ); # Mappinghash Facility my %Log2Syslog_Facility = ( "0" => "kernel", "1" => "user", "2" => "mail", "3" => "system", "4" => "security", "5" => "syslog", "6" => "printer", "7" => "network", "8" => "UUCP", "9" => "clock", "10" => "security", "11" => "FTP", "12" => "NTP", "13" => "log_audit", "14" => "log_alert", "15" => "clock", "16" => "local0", "17" => "local1", "18" => "local2", "19" => "local3", "20" => "local4", "21" => "local5", "22" => "local6", "23" => "local7" ); # Längenvorgaben nach RFC3164 my %RFC3164len = ("TAG" => 32, # max. Länge TAG-Feld "DL" => 1024 # max. Länge Message insgesamt ); # Längenvorgaben nach RFC5425 my %RFC5425len = ("DL" => 8192, # max. Länge Message insgesamt mit TLS "HST" => 255, # max. Länge Hostname "ID" => 48, # max. Länge APP-NAME bzw. Ident "PID" => 128, # max. Länge Proc-ID "MID" => 32 # max. Länge MSGID ); my %vHintsExt_en = ( "4" => "The guidelines for usage of RFC5425 Date and Time on the Internet: Timestamps", "3" => "The RFC5425 TLS Transport Protocol", "2" => "The basics of RFC3164 (BSD) protocol", "1" => "Informations about RFC5424 (IETF) syslog protocol" ); my %vHintsExt_de = ( "4" => "Die Richtlinien zur Verwendung von RFC5425 Datum und Zeit im Internet: Timestamps", "3" => "Die Beschreibung des RFC5425 TLS Transport Protokoll", "2" => "Die Grundlagen des RFC3164 (BSD) Protokolls", "1" => "Informationen über das RFC5424 (IETF) Syslog Protokoll" ); ############################################################################### sub Initialize { my ($hash) = @_; $hash->{DefFn} = \&Define; $hash->{UndefFn} = \&Undef; $hash->{DeleteFn} = \&Delete; $hash->{SetFn} = \&Set; $hash->{GetFn} = \&Get; $hash->{AttrFn} = \&Attr; $hash->{NotifyFn} = \&eventLog; $hash->{DbLog_splitFn} = \&DbLogSplit; $hash->{ReadFn} = \&Read; $hash->{AttrList} = "addStateEvent:1,0 ". "disable:1,0,maintenance ". "addTimestamp:0,1 ". "contDelimiter ". "exclErrCond:textField-long ". "logFormat:BSD,IETF ". "makeEvent:no,intern,reading ". "outputFields:sortable-strict,PRIVAL,FAC,SEV,TS,HOST,DATE,TIME,ID,PID,MID,SDFIELD,CONT ". "parseProfile:Automatic,BSD,IETF,TPLink-Switch,UniFi,raw,ParseFn ". "parseFn:textField-long ". "respectSeverity:multiple-strict,Emergency,Alert,Critical,Error,Warning,Notice,Informational,Debug ". "octetCount:1,0 ". "protocol:UDP,TCP ". "port ". "rateCalcRerun ". "ssldebug:0,1,2,3 ". "sslCertPrefix ". "TLS:1,0 ". "timeout ". "timeSpec:Local,UTC ". "useParsefilter:0,1 ". "useEOF:1,0 ". $readingFnAttributes ; eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html) return; } ############################################################################### sub Define { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $name = $hash->{NAME}; return "Error: Perl module ".$MissModulSocket." is missing. Install it on Debian with: sudo apt-get install libio-socket-multicast-perl" if($MissModulSocket); return "Error: Perl module ".$MissModulNDom." is missing." if($MissModulNDom); # Example Sender: define splunklog Log2Syslog splunk.myds.me ident:Prod event:.* fhem:.* # Example Collector: define SyslogServer Log2Syslog delete($hash->{HELPER}{EVNTLOG}); delete($hash->{HELPER}{FHEMLOG}); delete($hash->{HELPER}{IDENT}); $hash->{MYHOST} = hostname(); # eigener Host (lt. RFC nur Hostname f. BSD) my $myfqdn = hostfqdn(); # MYFQDN eigener Host (f. IETF) $myfqdn =~ s/\.$//x if($myfqdn); $hash->{MYFQDN} = $myfqdn // $hash->{MYHOST}; if(int(@a)-3 < 0){ # Einrichtung Servermode (Collector) $hash->{MODEL} = "Collector"; $hash->{PROFILE} = "Automatic"; readingsSingleUpdate ($hash, 'Parse_Err_No', 0, 1); # Fehlerzähler für Parse-Errors auf 0 readingsSingleUpdate ($hash, 'Parse_Err_LastData', 'n.a.', 0); Log3slog ($hash, 3, "Log2Syslog $name - entering Syslog servermode ..."); initServer ("$name,global"); } else { # Sendermode $hash->{MODEL} = "Sender"; setidrex($hash,$a[3]) if($a[3]); setidrex($hash,$a[4]) if($a[4]); setidrex($hash,$a[5]) if($a[5]); eval { "Hallo" =~ m/^$hash->{HELPER}{EVNTLOG}$/x } if($hash->{HELPER}{EVNTLOG}); return "Bad regexp: $@" if($@); eval { "Hallo" =~ m/^$hash->{HELPER}{FHEMLOG}$/x } if($hash->{HELPER}{FHEMLOG}); return "Bad regexp: $@" if($@); return "Bad regexp: starting with *" if((defined($hash->{HELPER}{EVNTLOG}) && $hash->{HELPER}{EVNTLOG} =~ m/^\*/x) || (defined($hash->{HELPER}{FHEMLOG}) && $hash->{HELPER}{FHEMLOG} =~ m/^\*/x)); # nur Events dieser Devices an NotifyFn weiterleiten, NOTIFYDEV wird gesetzt wenn möglich notifyRegexpChanged($hash, $hash->{HELPER}{EVNTLOG}) if($hash->{HELPER}{EVNTLOG}); $hash->{PEERHOST} = $a[2]; # Destination Host (Syslog Server) } $hash->{SEQNO} = 1; # PROCID in IETF, wird kontinuierlich hochgezählt $logInform{$hash->{NAME}} = \&fhemLog; # Funktion die in hash %loginform für $name eingetragen wird $hash->{HELPER}{SSLVER} = "n.a."; # Initialisierung $hash->{HELPER}{SSLALGO} = "n.a."; # Initialisierung $hash->{HELPER}{LTIME} = time(); # Init Timestmp f. Ratenbestimmung $hash->{HELPER}{OLDSEQNO} = $hash->{SEQNO}; # Init Sequenznummer f. Ratenbestimmung $hash->{HELPER}{OLDSTATE} = "initialized"; $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden # Versionsinformationen setzen setVersionInfo($hash); readingsBeginUpdate($hash); readingsBulkUpdate ($hash, "SSL_Version", "n.a."); readingsBulkUpdate ($hash, "SSL_Algorithm", "n.a."); readingsBulkUpdate ($hash, "Transfered_logs_per_minute", 0); readingsBulkUpdate ($hash, "state", "initialized") if($hash->{MODEL}=~/Sender/); readingsEndUpdate ($hash,1); calcTrate($hash); # regelm. Berechnung Transfer Rate starten return; } ################################################################################################# # Syslog Collector (Server-Mode) initialisieren # (im Collector Model) ################################################################################################# sub initServer { my ($a) = @_; my ($name,$global) = split(",",$a); my $hash = $defs{$name}; my $err; RemoveInternalTimer($hash, "FHEM::Log2Syslog::initServer"); return if(IsDisabled($name) || $hash->{SERVERSOCKET}); if($init_done != 1 || isMemLock($hash)) { InternalTimer(gettimeofday()+1, "FHEM::Log2Syslog::initServer", "$name,$global", 0); return; } # Inititialisierung FHEM ist fertig -> Attribute geladen my $port = AttrVal($name, "TLS", 0)?AttrVal($name, "port", 6514):AttrVal($name, "port", 1514); my $protocol = lc(AttrVal($name, "protocol", "udp")); my $lh = ($global ? ($global eq "global" ? undef : $global) : ($hash->{IPV6} ? "::1" : "127.0.0.1")); Log3slog ($hash, 3, "Log2Syslog $name - Opening socket on interface \"$global\" ..."); if($protocol =~ /udp/) { $hash->{SERVERSOCKET} = IO::Socket::INET->new( Domain => ($hash->{IPV6} ? AF_INET6() : AF_UNSPEC()), # Linux bug LocalHost => $lh, Proto => $protocol, LocalPort => $port, ReuseAddr => 1 ); if(!$hash->{SERVERSOCKET}) { $err = "Can't open Syslog Collector at $port: $!"; Log3slog ($hash, 1, "Log2Syslog $name - $err"); readingsSingleUpdate ($hash, 'state', $err, 1); return; } $hash->{FD} = $hash->{SERVERSOCKET}->fileno(); $hash->{PORT} = $hash->{SERVERSOCKET}->sockport(); } else { $lh = "global" if(!$lh); my $ret = TcpServer_Open($hash,$port,$lh); if($ret) { $err = "Can't open Syslog TCP Collector at $port: $ret"; Log3slog ($hash, 1, "Log2Syslog $name - $err"); readingsSingleUpdate ($hash, 'state', $err, 1); return; } } $hash->{PROTOCOL} = $protocol; $hash->{SEQNO} = 1; # PROCID wird kontinuierlich pro empfangenen Datensatz hochgezählt $hash->{HELPER}{OLDSEQNO} = $hash->{SEQNO}; # Init Sequenznummer f. Ratenbestimmung $hash->{INTERFACE} = $lh // "global"; Log3slog ($hash, 3, "Log2Syslog $name - port $hash->{PORT}/$protocol opened for Syslog Collector on interface \"$hash->{INTERFACE}\""); readingsSingleUpdate ($hash, "state", "initialized", 1); delete($readyfnlist{"$name.$port"}); $selectlist{"$name.$port"} = $hash; return; } ######################################################################################################## # Syslog Collector Daten empfangen (im Collector-Mode) # # !!!!! Achtung !!!!! # Kontextswitch des $hash beachten: initialer TCP-Server <-> temporärer TCP-Server ohne SERVERSOCKET # ######################################################################################################## # called from the global loop, when the select for hash->{FD} reports data sub Read { ## no critic 'complexity' my ($hash,$reread) = @_; my $socket = $hash->{SERVERSOCKET}; my ($err,$sev,$data,$ts,$phost,$pl,$ignore,$st,$len,$mlen,$evt,$pen,$rhash); return if($init_done != 1); # maximale Länge des (Syslog)-Frames als Begrenzung falls kein EOF # vom Sender initiiert wird (Endlosschleife vermeiden) # Grundeinstellungen $mlen = 16384; $len = 8192; if($hash->{TEMPORARY}) { my $sname = $hash->{SNAME}; $rhash = $defs{$sname}; } else { $rhash = $hash; } my $pp = $rhash->{PROFILE}; if($pp =~ /BSD/) { # Framelänge BSD-Format $len = $RFC3164len{DL}; } elsif ($pp =~ /IETF/) { # Framelänge IETF-Format $len = $RFC5425len{DL}; } if($hash->{TEMPORARY}) { # temporäre Instanz angelegt durch TcpServer_Accept ($st,$data,$hash) = getIfData($hash,$len,$mlen,$reread); } my $name = $hash->{NAME}; return if(IsDisabled($name) || isMemLock($hash)); my $mevt = AttrVal($name, "makeEvent", "intern"); # wie soll Reading/Event erstellt werden my $sevevt = AttrVal($name, "respectSeverity", "" ); # welcher Schweregrad soll berücksichtigt werden (default: alle) my $uef = AttrVal($name, "useEOF", 0 ); # verwende EOF if($socket) { ($st,$data,$hash) = getIfData($hash,$len,$mlen,$reread); } if($data) { # parse Payload my (@load,$ocount,$msg,$tail); if($data =~ /^(?(\d+?))\s(?(.*))/sx) { # Syslog Sätze mit Octet Count -> Transmission of Syslog Messages over TCP https://tools.ietf.org/html/rfc6587 Log3slog ($hash, 4, "Log2Syslog $name - Datagramm with Octet Count detected - prepare message for Parsing ... \n"); use bytes; my $i = 0; $ocount = $+{ocount}; $tail = $+{tail}; $msg = substr($tail,0,$ocount); push @load, $msg; if(length($tail) >= $ocount) { $tail = substr($tail,$ocount); } else { $tail = substr($tail,length($msg)); } Log3slog ($hash, 5, "Log2Syslog $name -> OCTETCOUNT$i: $ocount"); Log3slog ($hash, 5, "Log2Syslog $name -> MSG$i : $msg"); Log3slog ($hash, 5, "Log2Syslog $name -> LENGTH_MSG$i: ".length($msg)); Log3slog ($hash, 5, "Log2Syslog $name -> TAIL$i : $tail"); while($tail && $tail =~ /^(?(\d+?))\s(?(.*))/sx) { $i++; $ocount = $+{ocount}; $tail = $+{tail}; next if(!$tail); $msg = substr($tail,0,$ocount); push @load, $msg; if(length($tail) >= $ocount) { $tail = substr($tail,$ocount); } else { $tail = substr($tail,length($msg)); } Log3slog ($hash, 5, "Log2Syslog $name -> OCTETCOUNT$i: $ocount"); Log3slog ($hash, 5, "Log2Syslog $name -> MSG$i : $msg"); Log3slog ($hash, 5, "Log2Syslog $name -> LENGTH_MSG$i: ".length($msg)); Log3slog ($hash, 5, "Log2Syslog $name -> TAIL$i : $tail"); } } else { if($uef) { push @load, $data; } else { @load = split("[\r\n]",$data); } } for my $line (@load) { next if(!$line); ($err,$ignore,$sev,$phost,$ts,$pl) = parsePayload($hash,$line); $hash->{SEQNO}++; if($err) { $pen = ReadingsVal($name, "Parse_Err_No", 0); $pen++; readingsSingleUpdate($hash, 'Parse_Err_No', $pen, 1); $st = "parse error - see logfile"; } elsif ($ignore) { Log3slog ($hash, 5, "Log2Syslog $name -> dataset was ignored by parseFn"); } else { return if($sevevt && $sevevt !~ m/$sev/x); # Message nicht berücksichtigen $st = "active"; if($mevt =~ /intern/) { # kein Reading, nur Event $pl = "$phost: $pl"; Trigger($hash,$ts,$pl); } elsif ($mevt =~ /reading/x) { # Reading, Event abhängig von event-on-.* readingsSingleUpdate($hash, "MSG_$phost", $pl, 1); } else { # Reading ohne Event readingsSingleUpdate($hash, "MSG_$phost", $pl, 0); } } $evt = ($st eq $hash->{HELPER}{OLDSTATE})?0:1; readingsSingleUpdate($hash, "state", $st, $evt); $hash->{HELPER}{OLDSTATE} = $st; } } return; } ############################################################################### # Daten vom Interface holen # # Die einzige Aufgabe der Instanz mit SERVERSOCKET ist TcpServer_Accept # durchzufuehren (und evtl. noch Statistiken). Durch den Accept wird eine # weitere Instanz des gleichen Typs angelegt die eine Verbindung repraesentiert # und im ReadFn die eigentliche Arbeit macht: # # - ohne SERVERSOCKET dafuer mit CD/FD, PEER und PORT. CD/FD enthaelt den # neuen Filedeskriptor. # - mit TEMPORARY (damit es nicht gespeichert wird) # - SNAME verweist auf die "richtige" Instanz, damit man die Attribute # abfragen kann. # - TcpServer_Accept traegt den neuen Filedeskriptor in die globale %selectlist # ein. Damit wird ReadFn von fhem.pl/select mit dem temporaeren Instanzhash # aufgerufen, wenn Daten genau bei dieser Verbindung anstehen. # (sSiehe auch "list TYPE=FHEMWEB", bzw. "man -s2 accept") # ############################################################################### sub getIfData { ## no critic 'complexity' my $hash = shift; my $len = shift; my $mlen = shift; my $reread = shift; my $name = $hash->{NAME}; my $socket = $hash->{SERVERSOCKET}; my $protocol = lc(AttrVal($name, "protocol", "udp")); my ($eof,$buforun) = (0,0); if($hash->{TEMPORARY}) { # temporäre Instanz abgelegt durch TcpServer_Accept $protocol = "tcp"; } my $st = ReadingsVal($name,"state","active"); my ($data,$ret); if(!$reread) { if($socket && $protocol =~ /udp/) { # UDP Datagramm empfangen Log3slog ($hash, 4, "Log2Syslog $name - ####################################################### "); Log3slog ($hash, 4, "Log2Syslog $name - ######### new Syslog UDP Receive ######### "); Log3slog ($hash, 4, "Log2Syslog $name - ####################################################### "); unless($socket->recv($data, $len)) { Log3slog ($hash, 3, "Log2Syslog $name - Seq \"$hash->{SEQNO}\" invalid data: $data"); $data = '' if(length($data) == 0); $st = "receive error - see logfile"; } else { my $dl = length($data); Log3slog ($hash, 5, "Log2Syslog $name - Buffer ".$dl." chars ready to parse:\n$data"); } return ($st,$data,$hash); } elsif ($protocol =~ /tcp/) { if($hash->{SERVERSOCKET}) { # Accept and create a child my $nhash = TcpServer_Accept($hash, "Log2Syslog"); return ($st,$data,$hash) if(!$nhash); $nhash->{CD}->blocking(0); if($nhash->{SSL}) { my $sslver = $nhash->{CD}->get_sslversion(); my $sslalgo = $nhash->{CD}->get_fingerprint(); readingsSingleUpdate($hash, "SSL_Version", $sslver, 1); readingsSingleUpdate($hash, "SSL_Algorithm", $sslalgo, 1); } return ($st,$data,$hash); } # Child, $hash ist Hash der temporären Instanz, $shash und $sname von dem originalen Device my $sname = $hash->{SNAME}; my $cname = $hash->{NAME}; my $shash = $defs{$sname}; # Hash des Log2Syslog-Devices bei temporärer TCP-Serverinstanz my $uef = AttrVal($sname, "useEOF", 0); my $tlsv = ReadingsVal($sname,"SSL_Version",''); Log3slog ($shash, 4, "Log2Syslog $sname - ####################################################### "); Log3slog ($shash, 4, "Log2Syslog $sname - ######### new Syslog TCP Receive ######### "); Log3slog ($shash, 4, "Log2Syslog $sname - ####################################################### "); Log3slog ($shash, 4, "Log2Syslog $sname - await EOF: $uef, SSL: $tlsv"); Log3slog ($shash, 4, "Log2Syslog $sname - childname: $cname"); $st = ReadingsVal($sname,"state","active"); my $c = $hash->{CD}; if($c) { $shash->{HELPER}{TCPPADDR} = $hash->{PEER}; my $buf; my $off = 0; $ret = sysread($c, $buf, $len); # returns undef on error, 0 at end of file and Integer, number of bytes read on success. if(!defined($ret) && $! == EWOULDBLOCK()){ # error $hash->{wantWrite} = 1 if(TcpServer_WantWrite($hash)); $hash = $shash; Log3slog ($hash, 2, "Log2Syslog $sname - ERROR - TCP stack error: $!"); return ($st,undef,$hash); } elsif (!$ret) { # EOF or error Log3slog ($shash, 4, "Log2Syslog $sname - Connection closed for $cname: ".(defined($ret) ? 'EOF' : $!)); if(!defined($ret)) { # error CommandDelete(undef, $cname); $hash = $shash; return ($st,undef,$hash); } else { # EOF $eof = 1; $data = $hash->{BUF}; CommandDelete(undef, $cname); } } if(!$eof) { $hash->{BUF} .= $buf; Log3slog ($shash, 5, "Log2Syslog $sname - Add $ret chars to buffer:\n$buf") if($uef && !$hash->{SSL}); } if($hash->{SSL} && $c->can('pending')) { while($c->pending()) { sysread($c, $buf, 1024); $hash->{BUF} .= $buf; } } $buforun = (length($hash->{BUF}) >= $mlen)?1:0 if($hash->{BUF}); if(!$uef || $hash->{SSL} || $buforun) { $data = $hash->{BUF}; delete $hash->{BUF}; $hash = $shash; if($data) { my $dl = length($data); Log3slog ($shash, 2, "Log2Syslog $sname - WARNING - Buffer overrun ! Enforce parse data.") if($buforun); Log3slog ($shash, 5, "Log2Syslog $sname - Buffer $dl chars ready to parse:\n$data"); } return ($st,$data,$hash); } else { if($eof) { $hash = $shash; my $dl = length($data); Log3slog ($shash, 5, "Log2Syslog $sname - Buffer $dl chars after EOF ready to parse:\n$data") if($data); return ($st,$data,$hash); } } } } else { $st = "error - no socket opened"; $data = ''; return ($st,$data,$hash); } } return ($st,undef,$hash); } ############################################################################### # Parsen Payload für Syslog-Server # (im Collector Model) ############################################################################### sub parsePayload { ## no critic 'complexity' my ($hash,$data) = @_; my $name = $hash->{NAME}; my $pp = AttrVal($name, "parseProfile", $hash->{PROFILE}); my $severity = ""; my $facility = ""; my @evf = split q{,},AttrVal($name, "outputFields", "FAC,SEV,ID,CONT"); # auszugebene Felder im Event/Reading my $ignore = 0; my ($to,$Mmm,$dd,$day,$ietf,$err,$pl,$tail); $data = parseFilter($data) if(AttrVal($name,"useParsefilter",0)); # Steuerzeichen werden entfernt (Achtung auch CR/LF) Log3slog ($hash, 4, "Log2Syslog $name - ######### Parse Message ######### "); Log3slog ($hash, 5, "Log2Syslog $name - parse profile: $pp"); # Hash zur Umwandlung Felder in deren Variablen my ($ocount,$prival,$ts,$host,$date,$time,$id,$pid,$mid,$sdfield,$cont); my ($fac,$sev,$msec) = ("","",""); my %fh = (PRIVAL => \$prival, FAC => \$fac, SEV => \$sev, TS => \$ts, HOST => \$host, DATE => \$date, TIME => \$time, ID => \$id, PID => \$pid, MID => \$mid, SDFIELD => \$sdfield, CONT => \$cont, DATA => \$data ); my ($phost) = evalPeer($hash); # Sender Host / IP-Adresse ermitteln, $phost wird Reading im Event Log3slog ($hash, 4, "Log2Syslog $name - raw message -> $data"); my $year = strftime "%Y", localtime; # aktuelles Jahr if($pp =~ /^Automatic/x) { Log3slog($name, 4, "Log2Syslog $name - Analyze message format automatically ..."); $pp = "raw"; $data =~ /^<(?\d{1,3})>(?\w{3}).*$/x; $prival = $+{prival}; $tail = $+{tail}; # Test auf BSD-Format if($tail && " Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec " =~ /\s$tail\s/x) { $pp = "BSD"; } else { # Test auf IETF-Format $data =~ /^((?(\d+))\s)?<(?\d{1,3})>(?\d{0,2})\s?(?\d{4}-\d{2}-\d{2})T(?