# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import socket
from . NotifyBase import NotifyBase
from . . common import NotifyType
from . . utils import parse_bool
from . . AppriseLocale import gettext_lazy as _
class syslog :
"""
Extrapoloated information from the syslog library so that this plugin
would not be dependent on it .
"""
# Notification Categories
LOG_KERN = 0
LOG_USER = 8
LOG_MAIL = 16
LOG_DAEMON = 24
LOG_AUTH = 32
LOG_SYSLOG = 40
LOG_LPR = 48
LOG_NEWS = 56
LOG_UUCP = 64
LOG_CRON = 72
LOG_LOCAL0 = 128
LOG_LOCAL1 = 136
LOG_LOCAL2 = 144
LOG_LOCAL3 = 152
LOG_LOCAL4 = 160
LOG_LOCAL5 = 168
LOG_LOCAL6 = 176
LOG_LOCAL7 = 184
# Notification Types
LOG_INFO = 6
LOG_NOTICE = 5
LOG_WARNING = 4
LOG_CRIT = 2
class SyslogFacility :
"""
All of the supported facilities
"""
KERN = ' kern '
USER = ' user '
MAIL = ' mail '
DAEMON = ' daemon '
AUTH = ' auth '
SYSLOG = ' syslog '
LPR = ' lpr '
NEWS = ' news '
UUCP = ' uucp '
CRON = ' cron '
LOCAL0 = ' local0 '
LOCAL1 = ' local1 '
LOCAL2 = ' local2 '
LOCAL3 = ' local3 '
LOCAL4 = ' local4 '
LOCAL5 = ' local5 '
LOCAL6 = ' local6 '
LOCAL7 = ' local7 '
SYSLOG_FACILITY_MAP = {
SyslogFacility . KERN : syslog . LOG_KERN ,
SyslogFacility . USER : syslog . LOG_USER ,
SyslogFacility . MAIL : syslog . LOG_MAIL ,
SyslogFacility . DAEMON : syslog . LOG_DAEMON ,
SyslogFacility . AUTH : syslog . LOG_AUTH ,
SyslogFacility . SYSLOG : syslog . LOG_SYSLOG ,
SyslogFacility . LPR : syslog . LOG_LPR ,
SyslogFacility . NEWS : syslog . LOG_NEWS ,
SyslogFacility . UUCP : syslog . LOG_UUCP ,
SyslogFacility . CRON : syslog . LOG_CRON ,
SyslogFacility . LOCAL0 : syslog . LOG_LOCAL0 ,
SyslogFacility . LOCAL1 : syslog . LOG_LOCAL1 ,
SyslogFacility . LOCAL2 : syslog . LOG_LOCAL2 ,
SyslogFacility . LOCAL3 : syslog . LOG_LOCAL3 ,
SyslogFacility . LOCAL4 : syslog . LOG_LOCAL4 ,
SyslogFacility . LOCAL5 : syslog . LOG_LOCAL5 ,
SyslogFacility . LOCAL6 : syslog . LOG_LOCAL6 ,
SyslogFacility . LOCAL7 : syslog . LOG_LOCAL7 ,
}
SYSLOG_FACILITY_RMAP = {
syslog . LOG_KERN : SyslogFacility . KERN ,
syslog . LOG_USER : SyslogFacility . USER ,
syslog . LOG_MAIL : SyslogFacility . MAIL ,
syslog . LOG_DAEMON : SyslogFacility . DAEMON ,
syslog . LOG_AUTH : SyslogFacility . AUTH ,
syslog . LOG_SYSLOG : SyslogFacility . SYSLOG ,
syslog . LOG_LPR : SyslogFacility . LPR ,
syslog . LOG_NEWS : SyslogFacility . NEWS ,
syslog . LOG_UUCP : SyslogFacility . UUCP ,
syslog . LOG_CRON : SyslogFacility . CRON ,
syslog . LOG_LOCAL0 : SyslogFacility . LOCAL0 ,
syslog . LOG_LOCAL1 : SyslogFacility . LOCAL1 ,
syslog . LOG_LOCAL2 : SyslogFacility . LOCAL2 ,
syslog . LOG_LOCAL3 : SyslogFacility . LOCAL3 ,
syslog . LOG_LOCAL4 : SyslogFacility . LOCAL4 ,
syslog . LOG_LOCAL5 : SyslogFacility . LOCAL5 ,
syslog . LOG_LOCAL6 : SyslogFacility . LOCAL6 ,
syslog . LOG_LOCAL7 : SyslogFacility . LOCAL7 ,
}
# Used as a lookup when handling the Apprise -> Syslog Mapping
SYSLOG_PUBLISH_MAP = {
NotifyType . INFO : syslog . LOG_INFO ,
NotifyType . SUCCESS : syslog . LOG_NOTICE ,
NotifyType . FAILURE : syslog . LOG_CRIT ,
NotifyType . WARNING : syslog . LOG_WARNING ,
}
class NotifyRSyslog ( NotifyBase ) :
"""
A wrapper for Remote Syslog Notifications
"""
# The default descriptive name associated with the Notification
service_name = ' Remote Syslog '
# The services URL
service_url = ' https://tools.ietf.org/html/rfc5424 '
# The default protocol
protocol = ' rsyslog '
# A URL that takes you to the setup/help of the specific protocol
setup_url = ' https://github.com/caronc/apprise/wiki/Notify_rsyslog '
# Disable throttle rate for RSyslog requests
request_rate_per_sec = 0
# Define object templates
templates = (
' {schema} :// {host} ' ,
' {schema} :// {host} : {port} ' ,
' {schema} :// {host} / {facility} ' ,
' {schema} :// {host} : {port} / {facility} ' ,
)
# Define our template tokens
template_tokens = dict ( NotifyBase . template_tokens , * * {
' facility ' : {
' name ' : _ ( ' Facility ' ) ,
' type ' : ' choice:string ' ,
' values ' : [ k for k in SYSLOG_FACILITY_MAP . keys ( ) ] ,
' default ' : SyslogFacility . USER ,
' required ' : True ,
} ,
' host ' : {
' name ' : _ ( ' Hostname ' ) ,
' type ' : ' string ' ,
' required ' : True ,
} ,
' port ' : {
' name ' : _ ( ' Port ' ) ,
' type ' : ' int ' ,
' min ' : 1 ,
' max ' : 65535 ,
' default ' : 514 ,
} ,
} )
# Define our template arguments
template_args = dict ( NotifyBase . template_args , * * {
' facility ' : {
# We map back to the same element defined in template_tokens
' alias_of ' : ' facility ' ,
} ,
' logpid ' : {
' name ' : _ ( ' Log PID ' ) ,
' type ' : ' bool ' ,
' default ' : True ,
' map_to ' : ' log_pid ' ,
} ,
} )
def __init__ ( self , facility = None , log_pid = True , * * kwargs ) :
"""
Initialize RSyslog Object
"""
super ( ) . __init__ ( * * kwargs )
if facility :
try :
self . facility = SYSLOG_FACILITY_MAP [ facility ]
except KeyError :
msg = ' An invalid syslog facility ' \
' ( {} ) was specified. ' . format ( facility )
self . logger . warning ( msg )
raise TypeError ( msg )
else :
self . facility = \
SYSLOG_FACILITY_MAP [
self . template_tokens [ ' facility ' ] [ ' default ' ] ]
# Include PID with each message.
self . log_pid = log_pid
return
def send ( self , body , title = ' ' , notify_type = NotifyType . INFO , * * kwargs ) :
"""
Perform RSyslog Notification
"""
if title :
# Format title
body = ' {} : {} ' . format ( title , body )
# Always call throttle before any remote server i/o is made
self . throttle ( )
host = self . host
port = self . port if self . port \
else self . template_tokens [ ' port ' ] [ ' default ' ]
if self . log_pid :
payload = ' < %d >- %d - %s ' % (
SYSLOG_PUBLISH_MAP [ notify_type ] + self . facility * 8 ,
os . getpid ( ) , body )
else :
payload = ' < %d >- %s ' % (
SYSLOG_PUBLISH_MAP [ notify_type ] + self . facility * 8 , body )
# send UDP packet to upstream server
self . logger . debug (
' RSyslog Host: %s : %d / %s ' ,
host , port , SYSLOG_FACILITY_RMAP [ self . facility ] )
self . logger . debug ( ' RSyslog Payload: %s ' % str ( payload ) )
# our sent bytes
sent = 0
try :
sock = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM )
sock . settimeout ( self . socket_connect_timeout )
sent = sock . sendto ( payload . encode ( ' utf-8 ' ) , ( host , port ) )
sock . close ( )
except socket . gaierror as e :
self . logger . warning (
' A connection error occurred sending RSyslog '
' notification to %s : %d / %s ' , host , port ,
SYSLOG_FACILITY_RMAP [ self . facility ]
)
self . logger . debug ( ' Socket Exception: %s ' % str ( e ) )
return False
except socket . timeout as e :
self . logger . warning (
' A connection timeout occurred sending RSyslog '
' notification to %s : %d / %s ' , host , port ,
SYSLOG_FACILITY_RMAP [ self . facility ]
)
self . logger . debug ( ' Socket Exception: %s ' % str ( e ) )
return False
if sent < len ( payload ) :
self . logger . warning (
' RSyslog sent %d byte(s) but intended to send %d byte(s) ' ,
sent , len ( payload ) )
return False
self . logger . info ( ' Sent RSyslog notification. ' )
return True
def url ( self , privacy = False , * args , * * kwargs ) :
"""
Returns the URL built dynamically based on specified arguments .
"""
# Define any URL parameters
params = {
' logpid ' : ' yes ' if self . log_pid else ' no ' ,
}
# Extend our parameters
params . update ( self . url_parameters ( privacy = privacy , * args , * * kwargs ) )
return ' {schema} :// {hostname} {port} / {facility} /? {params} ' . format (
schema = self . protocol ,
hostname = NotifyRSyslog . quote ( self . host , safe = ' ' ) ,
port = ' ' if self . port is None
or self . port == self . template_tokens [ ' port ' ] [ ' default ' ]
else ' : {} ' . format ( self . port ) ,
facility = self . template_tokens [ ' facility ' ] [ ' default ' ]
if self . facility not in SYSLOG_FACILITY_RMAP
else SYSLOG_FACILITY_RMAP [ self . facility ] ,
params = NotifyRSyslog . urlencode ( params ) ,
)
@staticmethod
def parse_url ( url ) :
"""
Parses the URL and returns enough arguments that can allow
us to re - instantiate this object .
"""
results = NotifyBase . parse_url ( url , verify_host = False )
if not results :
# We're done early as we couldn't load the results
return results
tokens = [ ]
# Get our path values
tokens . extend ( NotifyRSyslog . split_path ( results [ ' fullpath ' ] ) )
# Initialization
facility = None
if tokens :
# Store the last entry as the facility
facility = tokens [ - 1 ] . lower ( )
# However if specified on the URL, that will over-ride what was
# identified
if ' facility ' in results [ ' qsd ' ] and len ( results [ ' qsd ' ] [ ' facility ' ] ) :
facility = results [ ' qsd ' ] [ ' facility ' ] . lower ( )
if facility and facility not in SYSLOG_FACILITY_MAP :
# Find first match; if no match is found we set the result
# to the matching key. This allows us to throw a TypeError
# during the __init__() call. The benifit of doing this
# check here is if we do have a valid match, we can support
# short form matches like 'u' which will match against user
facility = next ( ( f for f in SYSLOG_FACILITY_MAP . keys ( )
if f . startswith ( facility ) ) , facility )
# Save facility if set
if facility :
results [ ' facility ' ] = facility
# Include PID as part of the message logged
results [ ' log_pid ' ] = parse_bool (
results [ ' qsd ' ] . get (
' logpid ' ,
NotifyRSyslog . template_args [ ' logpid ' ] [ ' default ' ] ) )
return results