x-pack/filebeat/input/httpjson/value_tpl.go (445 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package httpjson import ( "bytes" "crypto/hmac" "crypto/sha1" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "hash" "net/url" "reflect" "regexp" "runtime" "strconv" "strings" "text/template" "time" "github.com/gofrs/uuid/v5" "github.com/elastic/beats/v7/libbeat/version" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/useragent" ) // we define custom delimiters to prevent issues when using template values as part of other Go templates. const ( leftDelim = "[[" rightDelim = "]]" ) var ( errEmptyTemplateResult = errors.New("the template result is empty") errExecutingTemplate = errors.New("the template execution failed") ) type valueTpl struct { *template.Template } func (t *valueTpl) Unpack(in string) error { tpl, err := template.New(""). Option("missingkey=error"). Funcs(template.FuncMap{ "add": add, "base64Decode": base64Decode, "base64DecodeNoPad": base64DecodeNoPad, "base64Encode": base64Encode, "base64EncodeNoPad": base64EncodeNoPad, "beatInfo": beatInfo, "div": div, "formatDate": formatDate, "getRFC5988Link": getRFC5988Link, "hash": hashStringHex, "hashBase64": hashStringBase64, "hexDecode": hexDecode, "hmac": hmacStringHex, "hmacBase64": hmacStringBase64, "join": join, "toJSON": toJSON, "max": max, "min": min, "mul": mul, "now": now, "parseDate": parseDate, "parseDateInTZ": parseDateInTZ, "parseDuration": parseDuration, "parseTimestamp": parseTimestamp, "parseTimestampMilli": parseTimestampMilli, "parseTimestampNano": parseTimestampNano, "replaceAll": replaceAll, "sprintf": fmt.Sprintf, "toInt": toInt, "urlEncode": urlEncode, "userAgent": userAgentString, "uuid": uuidString, }). Delims(leftDelim, rightDelim). Parse(in) if err != nil { return err } *t = valueTpl{Template: tpl} return nil } func (t *valueTpl) Execute(trCtx *transformContext, tr transformable, targetName string, defaultVal *valueTpl, log *logp.Logger) (val string, err error) { fallback := func(err error) (string, error) { if defaultVal != nil { log.Debugw("template execution: falling back to default value", "target", targetName) return defaultVal.Execute(emptyTransformContext(), transformable{}, targetName, nil, log) } return "", err } defer func() { if r := recover(); r != nil { val, err = fallback(errExecutingTemplate) } if err != nil { log.Debugw("template execution failed", "target", targetName, "error", err) } tryDebugTemplateValue(targetName, val, log) }() buf := new(bytes.Buffer) data := tr.Clone() data.Put("cursor", trCtx.cursorMap()) data.Put("first_event", trCtx.firstEventClone()) data.Put("last_event", trCtx.lastEventClone()) data.Put("last_response", trCtx.lastResponseClone().templateValues()) if trCtx.firstResponse != nil { data.Put("first_response", trCtx.firstResponseClone().templateValues()) } // This is only set when chaining is used if trCtx.parentTrCtx != nil { data.Put("parent_last_response", trCtx.parentTrCtx.lastResponseClone().templateValues()) } if err := t.Template.Execute(buf, data); err != nil { return fallback(err) } val = buf.String() if val == "" || strings.Contains(val, "<no value>") { return fallback(errEmptyTemplateResult) } return val, nil } func tryDebugTemplateValue(target, val string, log *logp.Logger) { switch target { case "Authorization", "Proxy-Authorization": // ignore filtered headers default: log.Debugw("evaluated template", "target", target, "value", val) } } const defaultTimeLayout = "RFC3339" var predefinedLayouts = map[string]string{ "ANSIC": time.ANSIC, "UnixDate": time.UnixDate, "RubyDate": time.RubyDate, "RFC822": time.RFC822, "RFC822Z": time.RFC822Z, "RFC850": time.RFC850, "RFC1123": time.RFC1123, "RFC1123Z": time.RFC1123Z, "RFC3339": time.RFC3339, "RFC3339Nano": time.RFC3339Nano, "Kitchen": time.Kitchen, } func now(add ...time.Duration) time.Time { now := timeNow().UTC() if len(add) == 0 { return now } return now.Add(add[0]) } func parseDuration(s string) time.Duration { d, _ := time.ParseDuration(s) return d } func parseDate(date string, layout ...string) time.Time { var ly string if len(layout) == 0 { ly = defaultTimeLayout } else { ly = layout[0] } if found := predefinedLayouts[ly]; found != "" { ly = found } t, err := time.Parse(ly, date) if err != nil { return time.Time{} } return t.UTC() } // parseDateInTZ parses a date string within a specified timezone, returning a time.Time // 'tz' is the timezone (offset or IANA name) for parsing func parseDateInTZ(date string, tz string, layout ...string) time.Time { var ly string if len(layout) == 0 { ly = defaultTimeLayout } else { ly = layout[0] } if found := predefinedLayouts[ly]; found != "" { ly = found } var loc *time.Location // Attempt to parse timezone as offset in various formats for _, format := range []string{"-07", "-0700", "-07:00"} { t, err := time.Parse(format, tz) if err != nil { continue } name, offset := t.Zone() loc = time.FixedZone(name, offset) break } // If parsing tz as offset fails, try loading location by name if loc == nil { var err error loc, err = time.LoadLocation(tz) if err != nil { loc = time.UTC // Default to UTC on error } } // Using Parse allows us not to worry about the timezone // as the predefined timezone is applied afterwards t, err := time.Parse(ly, date) if err != nil { return time.Time{} } // Manually create a new time object with the parsed date components and the desired location // It allows interpreting the parsed time in the specified timezone year, month, day := t.Date() hour, min, sec := t.Clock() nanosec := t.Nanosecond() localTime := time.Date(year, month, day, hour, min, sec, nanosec, loc) // Convert the time to UTC to standardize the output return localTime.UTC() } func formatDate(date time.Time, layouttz ...string) string { var layout, tz string switch { case len(layouttz) == 0: layout = defaultTimeLayout case len(layouttz) == 1: layout = layouttz[0] case len(layouttz) > 1: layout, tz = layouttz[0], layouttz[1] } if found := predefinedLayouts[layout]; found != "" { layout = found } if loc, err := time.LoadLocation(tz); err == nil { date = date.In(loc) } else { date = date.UTC() } return date.Format(layout) } func parseTimestamp(s int64) time.Time { return time.Unix(s, 0).UTC() } func parseTimestampMilli(ms int64) time.Time { return time.Unix(0, ms*1e6).UTC() } func parseTimestampNano(ns int64) time.Time { return time.Unix(0, ns).UTC() } var regexpLinkRel = regexp.MustCompile(`<(.*)>.*;\s*rel\=("[^"]*"|[^"][^;]*[^"])`) func getMatchLink(rel string, linksSplit []string) string { for _, link := range linksSplit { if !regexpLinkRel.MatchString(link) { continue } matches := regexpLinkRel.FindStringSubmatch(link) if len(matches) != 3 { continue } linkRel := matches[2] if len(linkRel) > 1 && linkRel[0] == '"' { // We can only have a leading quote if we also have a separate trailing quote. linkRel = linkRel[1 : len(linkRel)-1] } if linkRel != rel { continue } return matches[1] } return "" } func getRFC5988Link(rel string, links []string) string { if len(links) == 1 && strings.Count(links[0], "rel=") > 1 { linksSplit := strings.Split(links[0], ",") return getMatchLink(rel, linksSplit) } return getMatchLink(rel, links) } func toInt(v interface{}) int64 { vv := reflect.ValueOf(v) switch vv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return vv.Int() case reflect.Float32, reflect.Float64: return int64(vv.Float()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return int64(vv.Uint()) case reflect.String: f, _ := strconv.ParseFloat(vv.String(), 64) return int64(f) default: return 0 } } func add(vs ...int64) int64 { var sum int64 for _, v := range vs { sum += v } return sum } func mul(a, b int64) int64 { return a * b } func div(a, b int64) int64 { return a / b } func min(arg1, arg2 reflect.Value) (interface{}, error) { lessThan, err := lt(arg1, arg2) if err != nil { return nil, err } // arg1 is < arg2. if lessThan { return arg1.Interface(), nil } return arg2.Interface(), nil } func max(arg1, arg2 reflect.Value) (interface{}, error) { lessThan, err := lt(arg1, arg2) if err != nil { return nil, err } // arg1 is < arg2. if lessThan { return arg2.Interface(), nil } return arg1.Interface(), nil } func base64Encode(values ...string) string { data := strings.Join(values, "") if data == "" { return "" } return base64.StdEncoding.EncodeToString([]byte(data)) } func base64EncodeNoPad(values ...string) string { data := strings.Join(values, "") if data == "" { return "" } return base64.RawStdEncoding.EncodeToString([]byte(data)) } func base64Decode(enc string) string { dec, _ := base64.StdEncoding.DecodeString(enc) return string(dec) } func base64DecodeNoPad(enc string) string { dec, _ := base64.RawStdEncoding.DecodeString(enc) return string(dec) } func hmacString(hmacType string, hmacKey []byte, data string) []byte { if data == "" { return nil } // Create a new HMAC by defining the hash type and the key (as byte array) var mac hash.Hash switch hmacType { case "sha256": mac = hmac.New(sha256.New, hmacKey) case "sha1": mac = hmac.New(sha1.New, hmacKey) default: // Upstream config validation prevents this from happening. return nil } // Write Data to it mac.Write([]byte(data)) // Get result and encode as bytes return mac.Sum(nil) } func hmacStringHex(hmacType string, hmacKey string, values ...string) string { data := strings.Join(values[:], "") if data == "" { return "" } bytes := hmacString(hmacType, []byte(hmacKey), data) // Get result and encode as hexadecimal string return hex.EncodeToString(bytes) } func hmacStringBase64(hmacType string, hmacKey string, values ...string) string { data := strings.Join(values[:], "") if data == "" { return "" } bytes := hmacString(hmacType, []byte(hmacKey), data) // Get result and encode as base64 string return base64.StdEncoding.EncodeToString(bytes) } func hashStringHex(typ string, values ...string) string { // Get result and encode as hexadecimal string return hex.EncodeToString(hashStrings(typ, values)) } func hashStringBase64(typ string, values ...string) string { // Get result and encode as base64 string return base64.StdEncoding.EncodeToString(hashStrings(typ, values)) } func hashStrings(typ string, data []string) []byte { var h hash.Hash switch typ { case "sha256": h = sha256.New() case "sha1": h = sha1.New() default: // Upstream config validation prevents this from happening. return nil } for _, d := range data { h.Write([]byte(d)) } return h.Sum(nil) } func hexDecode(enc string) string { decodedString, err := hex.DecodeString(enc) if err != nil { return "" } return string(decodedString) } func uuidString() string { uuid, err := uuid.NewV4() if err != nil { return "" } return uuid.String() } // join concatenates the elements of its first argument to create a single string. The separator // string sep is placed between elements in the resulting string. If the first argument is not of // type string or []string, its elements will be stringified. func join(v interface{}, sep string) string { // check for []string or string to avoid using reflect switch t := v.(type) { case []string: return strings.Join(t, sep) case string: return t } // if we have a slice of a different type, convert it to []string switch reflect.TypeOf(v).Kind() { case reflect.Slice, reflect.Array: s := reflect.ValueOf(v) vs := make([]string, s.Len()) for i := 0; i < s.Len(); i++ { vs[i] = fmt.Sprint(s.Index(i)) } return strings.Join(vs, sep) } // return the stringified single value return fmt.Sprint(v) } func userAgentString(values ...string) string { return useragent.UserAgent("Filebeat", version.GetDefaultVersion(), version.Commit(), version.BuildTime().String(), values...) } func beatInfo() map[string]string { return map[string]string{ "goos": runtime.GOOS, "goarch": runtime.GOARCH, "commit": version.Commit(), "buildtime": version.BuildTime().String(), "version": version.GetDefaultVersion(), } } func urlEncode(value string) string { if value == "" { return "" } return url.QueryEscape(value) } // replaceAll returns a copy of the string s with all non-overlapping instances // of old replaced by new. // // Note that the order of the arguments differs from Go's [strings.ReplaceAll] to // make pipelining more ergonomic. This allows s to be piped in because it is // the final argument. For example, // // [[ "some value" | replaceAll "some" "my" ]] // == "my value" func replaceAll(old, new, s string) string { return strings.ReplaceAll(s, old, new) } // toJSON converts the given structure into a JSON string. func toJSON(i interface{}) (string, error) { result, err := json.Marshal(i) if err != nil { return "", fmt.Errorf("toJSON failed: %w", err) } return string(bytes.TrimSpace(result)), nil }