You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
9.3 KiB

package alarmdelivery
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"net/smtp"
"strconv"
"strings"
"time"
"github.com/gosnmp/gosnmp"
)
type Attempt struct {
DeliveryID string
Channel string
Request Request
}
type Transport interface {
Send(context.Context, Attempt) error
}
type TransportFunc func(context.Context, Attempt) error
func (f TransportFunc) Send(ctx context.Context, attempt Attempt) error { return f(ctx, attempt) }
type WebTransport struct{}
func (WebTransport) Send(context.Context, Attempt) error { return nil }
type SecretResolver interface{ Get(string) ([]byte, bool) }
type EmailTransport struct {
Secrets SecretResolver
DialContext DialContextFunc
}
func (t EmailTransport) Send(ctx context.Context, attempt Attempt) error {
target := attempt.Request.Email
if target == nil {
return errors.New("email target is not configured")
}
if err := validateEmailTarget(*target); err != nil {
return err
}
dial := t.DialContext
if dial == nil {
dial = (&net.Dialer{Timeout: 5 * time.Second}).DialContext
}
address := net.JoinHostPort(target.Host, strconv.Itoa(target.Port))
connection, err := dial(ctx, "tcp", address)
if err != nil {
return fmt.Errorf("connect SMTP server: %w", err)
}
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(10 * time.Second))
client, err := smtp.NewClient(connection, target.Host)
if err != nil {
return fmt.Errorf("initialize SMTP client: %w", err)
}
defer client.Close()
if target.STARTTLS {
if ok, _ := client.Extension("STARTTLS"); !ok {
return errors.New("SMTP server does not advertise STARTTLS")
}
if err := client.StartTLS(&tls.Config{MinVersion: tls.VersionTLS12, ServerName: target.Host}); err != nil {
return fmt.Errorf("start SMTP TLS: %w", err)
}
}
if target.Auth {
if t.Secrets == nil {
return errors.New("SMTP secret resolver is unavailable")
}
password, ok := t.Secrets.Get(target.PasswordKey)
if !ok {
return errors.New("SMTP password is not available")
}
if err := client.Auth(smtp.PlainAuth("", target.Username, string(password), target.Host)); err != nil {
return fmt.Errorf("authenticate SMTP client: %w", err)
}
}
if err := client.Mail(target.From); err != nil {
return err
}
recipients := append(append([]string(nil), target.To...), target.CC...)
for _, recipient := range recipients {
if err := client.Rcpt(recipient); err != nil {
return err
}
}
writer, err := client.Data()
if err != nil {
return err
}
subject := sanitizeHeader(target.Subject)
if subject == "" {
subject = "TERA console server alarm"
}
body := target.Content
if body == "" {
body = attempt.Request.Message
}
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\n", sanitizeHeader(target.From), strings.Join(target.To, ", "))
if len(target.CC) > 0 {
headers += fmt.Sprintf("Cc: %s\r\n", strings.Join(target.CC, ", "))
}
headers += fmt.Sprintf("Subject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n", subject)
if _, err = io.WriteString(writer, headers+body+"\r\n"); err != nil {
_ = writer.Close()
return err
}
if err = writer.Close(); err != nil {
return err
}
return client.Quit()
}
func sanitizeHeader(value string) string {
return strings.TrimSpace(strings.NewReplacer("\r", " ", "\n", " ", "\x00", "").Replace(value))
}
type SNMPTrapTransport struct {
Secrets SecretResolver
}
func (t SNMPTrapTransport) Send(ctx context.Context, attempt Attempt) error {
for _, target := range attempt.Request.SNMPTraps {
if err := validateSNMPTrapTarget(target); err != nil {
return err
}
params := &gosnmp.GoSNMP{
Target: target.Host, Port: uint16(target.Port), Transport: "udp",
Context: ctx, Timeout: 5 * time.Second, Retries: 0,
Logger: gosnmp.NewLogger(log.New(io.Discard, "", 0)),
}
var trap gosnmp.SnmpTrap
switch target.Version {
case "v1":
community, err := t.secret(target.TargetID, target.CredentialKey, "community")
if err != nil {
return err
}
params.Version, params.Community = gosnmp.Version1, community
trap = gosnmp.SnmpTrap{Enterprise: ".1.3.6.1.4.1.55555", AgentAddress: "0.0.0.0", GenericTrap: 6, SpecificTrap: 1, Timestamp: uint(time.Now().Unix()), Variables: alarmTrapVariables(attempt.Request.Message, false)}
case "v2c":
community, err := t.secret(target.TargetID, target.CredentialKey, "community")
if err != nil {
return err
}
params.Version, params.Community = gosnmp.Version2c, community
trap = gosnmp.SnmpTrap{Variables: alarmTrapVariables(attempt.Request.Message, true)}
case "v3":
if err := t.configureV3(params, target); err != nil {
return err
}
trap = gosnmp.SnmpTrap{Variables: alarmTrapVariables(attempt.Request.Message, true)}
}
if err := params.Connect(); err != nil {
return fmt.Errorf("connect SNMP target %s: %w", target.TargetID, err)
}
_, sendErr := params.SendTrap(trap)
_ = params.Close()
if sendErr != nil {
return fmt.Errorf("write SNMP trap %s: %w", target.TargetID, sendErr)
}
}
return nil
}
func (t SNMPTrapTransport) secret(targetID, key, name string) (string, error) {
if t.Secrets == nil {
return "", errors.New("SNMP secret resolver is unavailable")
}
value, ok := t.Secrets.Get(key)
if !ok || len(value) == 0 {
return "", fmt.Errorf("SNMP %s for %s is not available", name, targetID)
}
return string(value), nil
}
func (t SNMPTrapTransport) configureV3(params *gosnmp.GoSNMP, target SNMPTrapTarget) error {
authProtocol, flags := gosnmp.NoAuth, gosnmp.NoAuthNoPriv
var authPassphrase string
switch target.AuthProtocol {
case "none":
case "md5":
authProtocol, flags = gosnmp.MD5, gosnmp.AuthNoPriv
case "sha1":
authProtocol, flags = gosnmp.SHA, gosnmp.AuthNoPriv
case "sha256":
authProtocol, flags = gosnmp.SHA256, gosnmp.AuthNoPriv
default:
return fmt.Errorf("SNMPv3 target %s has unsupported auth protocol", target.TargetID)
}
if flags != gosnmp.NoAuthNoPriv {
var err error
authPassphrase, err = t.secret(target.TargetID, target.CredentialKey, "authentication credential")
if err != nil {
return err
}
}
privacyProtocol := gosnmp.NoPriv
var privacyPassphrase string
switch target.PrivacyProtocol {
case "none":
case "des":
privacyProtocol, flags = gosnmp.DES, gosnmp.AuthPriv
case "aes":
privacyProtocol, flags = gosnmp.AES, gosnmp.AuthPriv
default:
return fmt.Errorf("SNMPv3 target %s has unsupported privacy protocol", target.TargetID)
}
if flags == gosnmp.AuthPriv {
var err error
privacyPassphrase, err = t.secret(target.TargetID, target.PrivacyKeyRef, "privacy credential")
if err != nil {
return err
}
}
params.Version = gosnmp.Version3
params.SecurityModel = gosnmp.UserSecurityModel
params.MsgFlags = flags
params.ContextEngineID = target.EngineID
params.SecurityParameters = &gosnmp.UsmSecurityParameters{
AuthoritativeEngineID: target.EngineID, UserName: target.Username,
AuthenticationProtocol: authProtocol, AuthenticationPassphrase: authPassphrase,
PrivacyProtocol: privacyProtocol, PrivacyPassphrase: privacyPassphrase,
}
return nil
}
func alarmTrapVariables(message string, includeTrapOID bool) []gosnmp.SnmpPDU {
variables := make([]gosnmp.SnmpPDU, 0, 2)
if includeTrapOID {
variables = append(variables, gosnmp.SnmpPDU{Name: ".1.3.6.1.6.3.1.1.4.1.0", Type: gosnmp.ObjectIdentifier, Value: ".1.3.6.1.4.1.55555.1.1"})
}
return append(variables, gosnmp.SnmpPDU{Name: ".1.3.6.1.4.1.55555.1.2", Type: gosnmp.OctetString, Value: []byte(sanitizeSNMPMessage(message))})
}
func sanitizeSNMPMessage(value string) string {
value = sanitizeHeader(value)
runes := []rune(value)
if len(runes) > 512 {
value = string(runes[:512])
}
return value
}
type DialContextFunc func(context.Context, string, string) (net.Conn, error)
type SyslogTransport struct{ DialContext DialContextFunc }
var syslogFacilities = map[string]int{
"kern": 0, "user": 1, "mail": 2, "daemon": 3, "auth": 4, "syslog": 5,
"lpr": 6, "news": 7, "uucp": 8, "cron": 9, "authpriv": 10, "ftp": 11,
"local0": 16, "local1": 17, "local2": 18, "local3": 19,
"local4": 20, "local5": 21, "local6": 22, "local7": 23,
}
func (t SyslogTransport) Send(ctx context.Context, attempt Attempt) error {
if attempt.Request.Syslog == nil {
return errors.New("syslog target is not configured")
}
target, err := normalizeSyslogTarget(*attempt.Request.Syslog)
if err != nil {
return err
}
dial := t.DialContext
if dial == nil {
dialer := &net.Dialer{Timeout: 5 * time.Second}
dial = dialer.DialContext
}
connection, err := dial(ctx, target.Protocol, net.JoinHostPort(target.Host, strconv.Itoa(target.Port)))
if err != nil {
return fmt.Errorf("connect remote syslog: %w", err)
}
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(5 * time.Second))
priority := syslogFacilities[target.Facility]*8 + 4
message := sanitizeSyslogValue(attempt.Request.Message)
ruleID := sanitizeSyslogValue(attempt.Request.RuleID)
if ruleID == "" {
ruleID = "delivery-test"
}
payload := fmt.Sprintf("<%d>1 %s tera tera-alarmd - %s - %s\n", priority, time.Now().UTC().Format(time.RFC3339), ruleID, message)
if _, err := connection.Write([]byte(payload)); err != nil {
return fmt.Errorf("write remote syslog: %w", err)
}
return nil
}
func sanitizeSyslogValue(value string) string {
value = strings.TrimSpace(value)
value = strings.ReplaceAll(value, "\r", " ")
value = strings.ReplaceAll(value, "\n", " ")
value = strings.ReplaceAll(value, "\x00", "")
return value
}