diff --git a/.golangci.yaml b/.golangci.yaml index 3fa5cbe..a012e09 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -17,7 +17,6 @@ linters: - exportloopref # Finds pointers to range variables. - goconst # Finds repeated strings that could be replaced by a const. - gocyclo # Finds funcs with high cyclomatic complexity. - - godot # Finds comments that do not end with a period. - gofumpt # Finds files that were not gofumpt-ed. - gosec # Finds security problems. - lll # Finds lines that exceed the line length limit. diff --git a/rfc5424/model.go b/rfc5424/model.go index 6785017..ac759ac 100644 --- a/rfc5424/model.go +++ b/rfc5424/model.go @@ -2,6 +2,7 @@ package rfc5424 import "time" +// Message represents a syslog message as defined in RFC 5424. type Message struct { PRI PRI Version byte @@ -11,23 +12,28 @@ type Message struct { ProcID string MsgID string StructuredData string - StructuredDataElements *[]StructuredDataElements + StructuredDataElements *[]StructuredDataElement Message string } +// PRI represents the Priority value of a syslog message. +// The PRI is a single byte that encodes the facility and severity of the message. type PRI struct { value byte } +// Facility returns the facility value of the PRI. func (p PRI) Facility() byte { return p.value & 0xF8 >> 3 } +// Severity returns the severity value of the PRI. func (p PRI) Severity() byte { return p.value & 0x07 } -type StructuredDataElements struct { +// StructuredDataElement represents a structured data element in a syslog message. +type StructuredDataElement struct { ID string Parameters map[string]string } diff --git a/rfc5424/options.go b/rfc5424/options.go index 2d1ea2e..57101b8 100644 --- a/rfc5424/options.go +++ b/rfc5424/options.go @@ -1,9 +1,10 @@ package rfc5424 -type parseOption func(*RFC5424) +type parseOption func(*Parser) +// WithParseStructuredDataElements enables parsing of structured data elements into its seperate parts. func WithParseStructuredDataElements() parseOption { - return func(r *RFC5424) { + return func(r *Parser) { r.parseStructuredDataElements = true } } diff --git a/rfc5424/rfc5424.go b/rfc5424/rfc5424.go index 3813c66..d648f7e 100644 --- a/rfc5424/rfc5424.go +++ b/rfc5424/rfc5424.go @@ -8,23 +8,21 @@ import ( "github.com/ysmilda/syslog/pkg/characters" ) -const ( - nilValue = '-' -) - -type RFC5424 struct { +type Parser struct { parseStructuredDataElements bool } -func NewRFC5424(options ...parseOption) RFC5424 { - r := RFC5424{} +// NewParser creates a new Parser with the provided options. +func NewParser(options ...parseOption) Parser { + r := Parser{} for _, option := range options { option(&r) } return r } -func (r RFC5424) Parse(input io.ByteScanner) (Message, error) { +// Parse tries to parse a syslog message from the input. If the input is not a valid syslog message, an error is returned. +func (r Parser) Parse(input io.ByteScanner) (Message, error) { // Taken from https://datatracker.ietf.org/doc/html/rfc5424#section-6 // The syslog message has the following ABNF [RFC5234] definition: // SYSLOG-MSG = HEADER SP STRUCTURED-DATA [SP MSG] @@ -32,7 +30,7 @@ func (r RFC5424) Parse(input io.ByteScanner) (Message, error) { var ( m Message - elements *[]StructuredDataElements + elements *[]StructuredDataElement ) pri, err := parsePRI(input) @@ -105,10 +103,10 @@ func (r RFC5424) Parse(input io.ByteScanner) (Message, error) { }, nil } +// parsePRI parses the PRI part of a syslog message according to the following rules. +// PRI = "<" PRIVAL ">" +// PRIVAL = 1*3DIGIT ; range 0 .. 191 func parsePRI(input io.ByteScanner) (byte, error) { - // PRI = "<" PRIVAL ">" - // PRIVAL = 1*3DIGIT ; range 0 .. 191 - b, err := input.ReadByte() if err != nil || b != '<' { return 0, ErrInvalidPRI @@ -135,10 +133,10 @@ func parsePRI(input io.ByteScanner) (byte, error) { return 0, ErrInvalidPRI } +// parseVersion parses the VERSION part of a syslog message according to the following rules. +// VERSION = NONZERO-DIGIT 0*2DIGIT +// NONZERO-DIGIT = %d49-57 ; 1-9 func parseVersion(input io.ByteScanner) (byte, error) { - // VERSION = NONZERO-DIGIT 0*2DIGIT - // NONZERO-DIGIT = %d49-57 ; 1-9 - b, err := input.ReadByte() if err != nil { return 0, ErrInvalidVersion @@ -154,21 +152,22 @@ func parseVersion(input io.ByteScanner) (byte, error) { return b, nil } +// parseTimestamp parses the TIMESTAMP part of a syslog message according to the following rules. +// The TIMESTAMP field is a formalized timestamp derived from [RFC3339] +// TIMESTAMP = NILVALUE / FULL-DATE "T" FULL-TIME +// FULL-DATE = DATE-FULLYEAR "-" DATE-MONTH "-" DATE-MDAY +// DATE-FULLYEAR = 4DIGIT +// DATE-MONTH = 2DIGIT ; 01-12 +// DATE-MDAY = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year +// FULL-TIME = PARTIAL-TIME TIME-OFFSET +// PARTIAL-TIME = TIME-HOUR ":" TIME-MINUTE ":" TIME-SECOND [TIME-SECFRAC] +// TIME-HOUR = 2DIGIT ; 00-23 +// TIME-MINUTE = 2DIGIT ; 00-59 +// TIME-SECOND = 2DIGIT ; 00-59 +// TIME-SECFRAC = "." 1*6DIGIT +// TIME-OFFSET = "Z" / TIME-NUMOFFSET +// TIME-NUMOFFSET = ("+" / "-") TIME-HOUR ":" TIME-MINUTE func parseTimestamp(input io.ByteScanner) (time.Time, error) { - // The TIMESTAMP field is a formalized timestamp derived from [RFC3339] - // TIMESTAMP = NILVALUE / FULL-DATE "T" FULL-TIME - // FULL-DATE = DATE-FULLYEAR "-" DATE-MONTH "-" DATE-MDAY - // DATE-FULLYEAR = 4DIGIT - // DATE-MONTH = 2DIGIT ; 01-12 - // DATE-MDAY = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year - // FULL-TIME = PARTIAL-TIME TIME-OFFSET - // PARTIAL-TIME = TIME-HOUR ":" TIME-MINUTE ":" TIME-SECOND [TIME-SECFRAC] - // TIME-HOUR = 2DIGIT ; 00-23 - // TIME-MINUTE = 2DIGIT ; 00-59 - // TIME-SECOND = 2DIGIT ; 00-59 - // TIME-SECFRAC = "." 1*6DIGIT - // TIME-OFFSET = "Z" / TIME-NUMOFFSET - // TIME-NUMOFFSET = ("+" / "-") TIME-HOUR ":" TIME-MINUTE isNil, err := checkNilValue(input) if err != nil { @@ -193,34 +192,34 @@ func parseTimestamp(input io.ByteScanner) (time.Time, error) { return time.Parse(time.RFC3339, builder.String()) } +// parseHostname parses the HOSTNAME part of a syslog message according to the following rules. +// HOSTNAME = NILVALUE / 1*255PRINTUSASCII func parseHostname(input io.ByteScanner) (string, error) { - // HOSTNAME = NILVALUE / 1*255PRINTUSASCII - return parseString(input, 255, ErrInvalidHostname) } +// parseAppName parses the APP-NAME part of a syslog message according to the following rules. +// APP-NAME = NILVALUE / 1*48PRINTUSASCII func parseAppName(input io.ByteScanner) (string, error) { - // APP-NAME = NILVALUE / 1*48PRINTUSASCII - return parseString(input, 48, ErrInvalidAppName) } +// parseProcID parses the PROCID part of a syslog message according to the following rules. +// PROCID = NILVALUE / 1*128PRINTUSASCII func parseProcID(input io.ByteScanner) (string, error) { - // PROCID = NILVALUE / 1*128PRINTUSASCII - return parseString(input, 128, ErrInvalidProcID) } +// parseMsgID parses the MSGID part of a syslog message according to the following rules. +// MSGID = NILVALUE / 1*32PRINTUSASCII func parseMsgID(input io.ByteScanner) (string, error) { - // MSGID = NILVALUE / 1*32PRINTUSASCII - return parseString(input, 32, ErrInvalidMsgID) } +// parseStructuredData parses the STRUCTURED-DATA part of a syslog message into a string according to the following rules. +// STRUCTURED-DATA = NILVALUE / 1*SD-ELEMENT +// SD-ELEMENT = "[" SD-ID *(SP SD-PARAM) "]" func parseStructuredData(input io.ByteScanner) (string, error) { - // STRUCTURED-DATA = NILVALUE / 1*SD-ELEMENT - // SD-ELEMENT = "[" SD-ID *(SP SD-PARAM) "]" - isNil, err := checkNilValue(input) if err != nil { return "", ErrInvalidStructuredData @@ -250,14 +249,14 @@ func parseStructuredData(input io.ByteScanner) (string, error) { return builder.String(), nil } -func parseStructuredDataElements(input string) (*[]StructuredDataElements, error) { - // SD-ELEMENT = "[" SD-ID *(SP SD-PARAM) "]" - // SD-PARAM = PARAM-NAME "=" %d34 PARAM-VALUE %d34 - // SD-ID = SD-NAME - // PARAM-NAME = SD-NAME - // PARAM-VALUE = UTF-8-STRING ; characters '"', '\' and ']' MUST be escaped. - // SD-NAME = 1*32PRINTUSASCII except '=', SP, ']', %d34 (") - +// parseStructuredDataElements parses the STRUCTURED-DATA part of a syslog message according to the following rules. +// SD-ELEMENT = "[" SD-ID *(SP SD-PARAM) "]" +// SD-PARAM = PARAM-NAME "=" %d34 PARAM-VALUE %d34 +// SD-ID = SD-NAME +// PARAM-NAME = SD-NAME +// PARAM-VALUE = UTF-8-STRING ; characters '"', '\' and ']' MUST be escaped. +// SD-NAME = 1*32PRINTUSASCII except '=', SP, ']', %d34 (") +func parseStructuredDataElements(input string) (*[]StructuredDataElement, error) { // If the input is empty, return nil // The structured data is optional, and a nil value ('-') is parsed as an empty string. if input == "" { @@ -265,7 +264,7 @@ func parseStructuredDataElements(input string) (*[]StructuredDataElements, error } input = strings.TrimSpace(input) - elements := []StructuredDataElements{} + elements := []StructuredDataElement{} // Split the input by the indicator of a new element: '[' for _, element := range strings.Split(input, "[") { if element == "" { @@ -291,7 +290,7 @@ func parseStructuredDataElements(input string) (*[]StructuredDataElements, error value = strings.ReplaceAll(value, "\\]", "]") params[parts[0]] = value } - elements = append(elements, StructuredDataElements{ + elements = append(elements, StructuredDataElement{ ID: id, Parameters: params, }) @@ -299,6 +298,9 @@ func parseStructuredDataElements(input string) (*[]StructuredDataElements, error return &elements, nil } +// parseString parses a string from the input with a maximum length according to the following rules. +// STRING = NILVALUE / 1*[max]PRINTUSASCII SP + func parseString(input io.ByteScanner, max int, e error) (string, error) { isNil, err := checkNilValue(input) if err != nil { @@ -324,12 +326,14 @@ func parseString(input io.ByteScanner, max int, e error) (string, error) { return builder.String(), nil } +// checkNilValue checks if the input is a nil value ('-') according to the following rules. +// NILVALUE = "-" func checkNilValue(input io.ByteScanner) (bool, error) { b, err := input.ReadByte() if err != nil { return false, ErrInvalidNilValue } - if b == nilValue { + if b == '-' { space, err := input.ReadByte() if err != nil || space != ' ' { return false, ErrInvalidNilValue diff --git a/rfc5424/rfc5424_test.go b/rfc5424/rfc5424_test.go index 421cfc9..b399b4a 100644 --- a/rfc5424/rfc5424_test.go +++ b/rfc5424/rfc5424_test.go @@ -75,7 +75,7 @@ func TestParse(t *testing.T) { } for _, tc := range testcases { - r := NewRFC5424() + r := NewParser() msg, err := r.Parse(bytes.NewReader(tc.msg)) assert.Equal(t, tc.expectedMessage, msg, tc.name) assert.Equal(t, tc.expectedError, err, tc.name) @@ -475,7 +475,7 @@ func TestParseStructuredDataElements(t *testing.T) { testcases := []struct { name string msg []byte - expectedSD *[]StructuredDataElements + expectedSD *[]StructuredDataElement expectedError error }{ { @@ -486,7 +486,7 @@ func TestParseStructuredDataElements(t *testing.T) { { name: "valid structured-data-elements - example 1", msg: []byte("[exampleSDID@32473 iut=\"3\" eventSource=\"Application\" eventID=\"1011\"] "), - expectedSD: &[]StructuredDataElements{ + expectedSD: &[]StructuredDataElement{ { ID: "exampleSDID@32473", Parameters: map[string]string{ @@ -500,7 +500,7 @@ func TestParseStructuredDataElements(t *testing.T) { { name: "valid structured-data-elements - example 2", msg: []byte("[exampleSDID@32473 iut=\"3\" eventSource=\"Application\" eventID=\"1011\"][examplePriority@32473 class=\"high\"] "), - expectedSD: &[]StructuredDataElements{ + expectedSD: &[]StructuredDataElement{ { ID: "exampleSDID@32473", Parameters: map[string]string{ @@ -631,7 +631,7 @@ func TestCheckNilValue(t *testing.T) { } func BenchmarkParse(b *testing.B) { - r := RFC5424{} + r := Parser{} msg := []byte("<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut=\"3\" eventSource=\"Application\" eventID=\"1011\"] An application event log entry...") for i := 0; i < b.N; i++ { _, err := r.Parse(bytes.NewReader(msg))