// Note to self : never try to code while looking after your kids // The result might look like this : https://pbs.twimg.com/media/BXqSuYXIEAAscVA.png package rfc5424 import ( "fmt" "math" "strconv" "time" "gopkg.in/mcuadros/go-syslog.v2/internal/syslogparser" ) const ( NILVALUE = '-' ) var ( ErrYearInvalid = &syslogparser.ParserError{"Invalid year in timestamp"} ErrMonthInvalid = &syslogparser.ParserError{"Invalid month in timestamp"} ErrDayInvalid = &syslogparser.ParserError{"Invalid day in timestamp"} ErrHourInvalid = &syslogparser.ParserError{"Invalid hour in timestamp"} ErrMinuteInvalid = &syslogparser.ParserError{"Invalid minute in timestamp"} ErrSecondInvalid = &syslogparser.ParserError{"Invalid second in timestamp"} ErrSecFracInvalid = &syslogparser.ParserError{"Invalid fraction of second in timestamp"} ErrTimeZoneInvalid = &syslogparser.ParserError{"Invalid time zone in timestamp"} ErrInvalidTimeFormat = &syslogparser.ParserError{"Invalid time format"} ErrInvalidAppName = &syslogparser.ParserError{"Invalid app name"} ErrInvalidProcId = &syslogparser.ParserError{"Invalid proc ID"} ErrInvalidMsgId = &syslogparser.ParserError{"Invalid msg ID"} ErrNoStructuredData = &syslogparser.ParserError{"No structured data"} ) type Parser struct { buff []byte cursor int l int header header structuredData string message string } type header struct { priority syslogparser.Priority version int timestamp time.Time hostname string appName string procId string msgId string } type partialTime struct { hour int minute int seconds int secFrac float64 } type fullTime struct { pt partialTime loc *time.Location } type fullDate struct { year int month int day int } func NewParser(buff []byte) *Parser { return &Parser{ buff: buff, cursor: 0, l: len(buff), } } func (p *Parser) Location(location *time.Location) { // Ignore as RFC5424 syslog always has a timezone } func (p *Parser) Parse() error { hdr, err := p.parseHeader() if err != nil { return err } p.header = hdr sd, err := p.parseStructuredData() if err != nil { return err } p.structuredData = sd p.cursor++ if p.cursor < p.l { p.message = string(p.buff[p.cursor:]) } return nil } func (p *Parser) Dump() syslogparser.LogParts { return syslogparser.LogParts{ "priority": p.header.priority.P, "facility": p.header.priority.F.Value, "severity": p.header.priority.S.Value, "version": p.header.version, "timestamp": p.header.timestamp, "hostname": p.header.hostname, "app_name": p.header.appName, "proc_id": p.header.procId, "msg_id": p.header.msgId, "structured_data": p.structuredData, "message": p.message, } } // HEADER = PRI VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID func (p *Parser) parseHeader() (header, error) { hdr := header{} pri, err := p.parsePriority() if err != nil { return hdr, err } hdr.priority = pri ver, err := p.parseVersion() if err != nil { return hdr, err } hdr.version = ver p.cursor++ ts, err := p.parseTimestamp() if err != nil { return hdr, err } hdr.timestamp = ts p.cursor++ host, err := p.parseHostname() if err != nil { return hdr, err } hdr.hostname = host p.cursor++ appName, err := p.parseAppName() if err != nil { return hdr, err } hdr.appName = appName p.cursor++ procId, err := p.parseProcId() if err != nil { return hdr, nil } hdr.procId = procId p.cursor++ msgId, err := p.parseMsgId() if err != nil { return hdr, nil } hdr.msgId = msgId p.cursor++ return hdr, nil } func (p *Parser) parsePriority() (syslogparser.Priority, error) { return syslogparser.ParsePriority(p.buff, &p.cursor, p.l) } func (p *Parser) parseVersion() (int, error) { return syslogparser.ParseVersion(p.buff, &p.cursor, p.l) } // https://tools.ietf.org/html/rfc5424#section-6.2.3 func (p *Parser) parseTimestamp() (time.Time, error) { var ts time.Time if p.cursor >= p.l { return ts, ErrInvalidTimeFormat } if p.buff[p.cursor] == NILVALUE { p.cursor++ return ts, nil } fd, err := parseFullDate(p.buff, &p.cursor, p.l) if err != nil { return ts, err } if p.cursor >= p.l || p.buff[p.cursor] != 'T' { return ts, ErrInvalidTimeFormat } p.cursor++ ft, err := parseFullTime(p.buff, &p.cursor, p.l) if err != nil { return ts, syslogparser.ErrTimestampUnknownFormat } nSec, err := toNSec(ft.pt.secFrac) if err != nil { return ts, err } ts = time.Date( fd.year, time.Month(fd.month), fd.day, ft.pt.hour, ft.pt.minute, ft.pt.seconds, nSec, ft.loc, ) return ts, nil } // HOSTNAME = NILVALUE / 1*255PRINTUSASCII func (p *Parser) parseHostname() (string, error) { return syslogparser.ParseHostname(p.buff, &p.cursor, p.l) } // APP-NAME = NILVALUE / 1*48PRINTUSASCII func (p *Parser) parseAppName() (string, error) { return parseUpToLen(p.buff, &p.cursor, p.l, 48, ErrInvalidAppName) } // PROCID = NILVALUE / 1*128PRINTUSASCII func (p *Parser) parseProcId() (string, error) { return parseUpToLen(p.buff, &p.cursor, p.l, 128, ErrInvalidProcId) } // MSGID = NILVALUE / 1*32PRINTUSASCII func (p *Parser) parseMsgId() (string, error) { return parseUpToLen(p.buff, &p.cursor, p.l, 32, ErrInvalidMsgId) } func (p *Parser) parseStructuredData() (string, error) { return parseStructuredData(p.buff, &p.cursor, p.l) } // ---------------------------------------------- // https://tools.ietf.org/html/rfc5424#section-6 // ---------------------------------------------- // XXX : bind them to Parser ? // FULL-DATE : DATE-FULLYEAR "-" DATE-MONTH "-" DATE-MDAY func parseFullDate(buff []byte, cursor *int, l int) (fullDate, error) { var fd fullDate year, err := parseYear(buff, cursor, l) if err != nil { return fd, err } if *cursor >= l || buff[*cursor] != '-' { return fd, syslogparser.ErrTimestampUnknownFormat } *cursor++ month, err := parseMonth(buff, cursor, l) if err != nil { return fd, err } if *cursor >= l || buff[*cursor] != '-' { return fd, syslogparser.ErrTimestampUnknownFormat } *cursor++ day, err := parseDay(buff, cursor, l) if err != nil { return fd, err } fd = fullDate{ year: year, month: month, day: day, } return fd, nil } // DATE-FULLYEAR = 4DIGIT func parseYear(buff []byte, cursor *int, l int) (int, error) { yearLen := 4 if *cursor+yearLen > l { return 0, syslogparser.ErrEOL } // XXX : we do not check for a valid year (ie. 1999, 2013 etc) // XXX : we only checks the format is correct sub := string(buff[*cursor : *cursor+yearLen]) *cursor += yearLen year, err := strconv.Atoi(sub) if err != nil { return 0, ErrYearInvalid } return year, nil } // DATE-MONTH = 2DIGIT ; 01-12 func parseMonth(buff []byte, cursor *int, l int) (int, error) { return syslogparser.Parse2Digits(buff, cursor, l, 1, 12, ErrMonthInvalid) } // DATE-MDAY = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year func parseDay(buff []byte, cursor *int, l int) (int, error) { // XXX : this is a relaxed constraint // XXX : we do not check if valid regarding February or leap years // XXX : we only checks that day is in range [01 -> 31] // XXX : in other words this function will not rant if you provide Feb 31th return syslogparser.Parse2Digits(buff, cursor, l, 1, 31, ErrDayInvalid) } // FULL-TIME = PARTIAL-TIME TIME-OFFSET func parseFullTime(buff []byte, cursor *int, l int) (fullTime, error) { var loc = new(time.Location) var ft fullTime pt, err := parsePartialTime(buff, cursor, l) if err != nil { return ft, err } loc, err = parseTimeOffset(buff, cursor, l) if err != nil { return ft, err } ft = fullTime{ pt: pt, loc: loc, } return ft, nil } // PARTIAL-TIME = TIME-HOUR ":" TIME-MINUTE ":" TIME-SECOND[TIME-SECFRAC] func parsePartialTime(buff []byte, cursor *int, l int) (partialTime, error) { var pt partialTime hour, minute, err := getHourMinute(buff, cursor, l) if err != nil { return pt, err } if *cursor >= l || buff[*cursor] != ':' { return pt, ErrInvalidTimeFormat } *cursor++ // ---- seconds, err := parseSecond(buff, cursor, l) if err != nil { return pt, err } pt = partialTime{ hour: hour, minute: minute, seconds: seconds, } // ---- if *cursor >= l || buff[*cursor] != '.' { return pt, nil } *cursor++ secFrac, err := parseSecFrac(buff, cursor, l) if err != nil { return pt, nil } pt.secFrac = secFrac return pt, nil } // TIME-HOUR = 2DIGIT ; 00-23 func parseHour(buff []byte, cursor *int, l int) (int, error) { return syslogparser.Parse2Digits(buff, cursor, l, 0, 23, ErrHourInvalid) } // TIME-MINUTE = 2DIGIT ; 00-59 func parseMinute(buff []byte, cursor *int, l int) (int, error) { return syslogparser.Parse2Digits(buff, cursor, l, 0, 59, ErrMinuteInvalid) } // TIME-SECOND = 2DIGIT ; 00-59 func parseSecond(buff []byte, cursor *int, l int) (int, error) { return syslogparser.Parse2Digits(buff, cursor, l, 0, 59, ErrSecondInvalid) } // TIME-SECFRAC = "." 1*6DIGIT func parseSecFrac(buff []byte, cursor *int, l int) (float64, error) { maxDigitLen := 6 max := *cursor + maxDigitLen from := *cursor to := from for to = from; to < max; to++ { if to >= l { break } c := buff[to] if !syslogparser.IsDigit(c) { break } } sub := string(buff[from:to]) if len(sub) == 0 { return 0, ErrSecFracInvalid } secFrac, err := strconv.ParseFloat("0."+sub, 64) *cursor = to if err != nil { return 0, ErrSecFracInvalid } return secFrac, nil } // TIME-OFFSET = "Z" / TIME-NUMOFFSET func parseTimeOffset(buff []byte, cursor *int, l int) (*time.Location, error) { if *cursor >= l || buff[*cursor] == 'Z' { *cursor++ return time.UTC, nil } return parseNumericalTimeOffset(buff, cursor, l) } // TIME-NUMOFFSET = ("+" / "-") TIME-HOUR ":" TIME-MINUTE func parseNumericalTimeOffset(buff []byte, cursor *int, l int) (*time.Location, error) { var loc = new(time.Location) sign := buff[*cursor] if (sign != '+') && (sign != '-') { return loc, ErrTimeZoneInvalid } *cursor++ hour, minute, err := getHourMinute(buff, cursor, l) if err != nil { return loc, err } tzStr := fmt.Sprintf("%s%02d:%02d", string(sign), hour, minute) tmpTs, err := time.Parse("-07:00", tzStr) if err != nil { return loc, err } return tmpTs.Location(), nil } func getHourMinute(buff []byte, cursor *int, l int) (int, int, error) { hour, err := parseHour(buff, cursor, l) if err != nil { return 0, 0, err } if *cursor >= l || buff[*cursor] != ':' { return 0, 0, ErrInvalidTimeFormat } *cursor++ minute, err := parseMinute(buff, cursor, l) if err != nil { return 0, 0, err } return hour, minute, nil } func toNSec(sec float64) (int, error) { _, frac := math.Modf(sec) fracStr := strconv.FormatFloat(frac, 'f', 9, 64) fracInt, err := strconv.Atoi(fracStr[2:]) if err != nil { return 0, err } return fracInt, nil } // ------------------------------------------------ // https://tools.ietf.org/html/rfc5424#section-6.3 // ------------------------------------------------ func parseStructuredData(buff []byte, cursor *int, l int) (string, error) { var sdData string var found bool if *cursor >= l { return "-", nil } if buff[*cursor] == NILVALUE { *cursor++ return "-", nil } if buff[*cursor] != '[' { return sdData, ErrNoStructuredData } from := *cursor to := from for to = from; to < l; to++ { if found { break } b := buff[to] if b == ']' { switch t := to + 1; { case t == l: found = true case t <= l && buff[t] == ' ': found = true } } } if found { *cursor = to return string(buff[from:to]), nil } return sdData, ErrNoStructuredData } func parseUpToLen(buff []byte, cursor *int, l int, maxLen int, e error) (string, error) { var to int var found bool var result string max := *cursor + maxLen for to = *cursor; (to <= max) && (to < l); to++ { if buff[to] == ' ' { found = true break } } if found { result = string(buff[*cursor:to]) } else if to > max { to = max // don't go past max } *cursor = to if found { return result, nil } return "", e }