vikunja/pkg/audit/sinks/syslog.go

117 lines
3.3 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package sinks
import (
"context"
"fmt"
"net"
"net/url"
"os"
"strings"
"sync"
"time"
)
// Hand-rolled RFC 5424 instead of log/syslog: the stdlib package only emits
// the older RFC 3164 format and does not build on Windows.
type Syslog struct {
network string
address string
facility int
hostname string
procid string
mu sync.Mutex
conn net.Conn
}
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,
}
// NewSyslog creates a syslog sink. The address has the form
// udp://host:port or tcp://host:port; the scheme defaults to udp.
func NewSyslog(address, facility string) (*Syslog, error) {
if address == "" {
return nil, fmt.Errorf("syslog forwarder requires an address")
}
if !strings.Contains(address, "://") {
address = "udp://" + address
}
u, err := url.Parse(address)
if err != nil {
return nil, fmt.Errorf("invalid syslog address %q: %w", address, err)
}
if u.Scheme != "udp" && u.Scheme != "tcp" {
return nil, fmt.Errorf("unsupported syslog scheme %q, must be udp or tcp", u.Scheme)
}
if facility == "" {
facility = "local0"
}
facilityCode, ok := syslogFacilities[strings.ToLower(facility)]
if !ok {
return nil, fmt.Errorf("unknown syslog facility %q", facility)
}
hostname, err := os.Hostname()
if err != nil || hostname == "" {
hostname = "-"
}
return &Syslog{
network: u.Scheme,
address: u.Host,
facility: facilityCode,
hostname: hostname,
procid: fmt.Sprintf("%d", os.Getpid()),
}, nil
}
func (s *Syslog) Write(line []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.conn == nil {
dialer := &net.Dialer{Timeout: 10 * time.Second}
conn, err := dialer.DialContext(context.Background(), s.network, s.address)
if err != nil {
return fmt.Errorf("could not connect to syslog at %s://%s: %w", s.network, s.address, err)
}
s.conn = conn
}
pri := s.facility*8 + 6 // severity: informational
frame := fmt.Sprintf("<%d>1 %s %s vikunja %s audit - %s",
pri, time.Now().UTC().Format(time.RFC3339Nano), s.hostname, s.procid, line)
if s.network == "tcp" {
frame += "\n" // RFC 6587 non-transparent framing
}
if _, err := s.conn.Write([]byte(frame)); err != nil {
// Drop the connection so the next write redials.
_ = s.conn.Close()
s.conn = nil
return err
}
return nil
}