/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ package agents import ( "errors" "fmt" "net" "os" "path/filepath" "strconv" "strings" "time" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" "github.com/fiorix/go-diameter/diam" "github.com/fiorix/go-diameter/diam/avp" "github.com/fiorix/go-diameter/diam/datatype" "github.com/fiorix/go-diameter/diam/dict" ) func loadDictionaries(dictsDir, componentId string) error { fi, err := os.Stat(dictsDir) if err != nil { if strings.HasSuffix(err.Error(), "no such file or directory") { return fmt.Errorf("<%s> Invalid dictionaries folder: <%s>", componentId, dictsDir) } return err } else if !fi.IsDir() { // If config dir defined, needs to exist return fmt.Errorf(" Path: <%s> is not a directory", dictsDir) } return filepath.Walk(dictsDir, func(path string, info os.FileInfo, err error) error { if !info.IsDir() { return nil } cfgFiles, err := filepath.Glob(filepath.Join(path, "*.xml")) // Only consider .xml files if err != nil { return err } if cfgFiles == nil { // No need of processing further since there are no dictionary files in the folder return nil } for _, filePath := range cfgFiles { utils.Logger.Info(fmt.Sprintf("<%s> Loading dictionary out of file %s", componentId, filePath)) if err := dict.Default.LoadFile(filePath); err != nil { return err } } return nil }) } // diamAVPValue will extract the go primary value out of diameter type value func diamAVPAsIface(dAVP *diam.AVP) (val interface{}, err error) { if dAVP == nil { return nil, errors.New("nil AVP") } switch dAVP.Data.Type() { default: return nil, fmt.Errorf("unsupported AVP data type: %d", dAVP.Data.Type()) case datatype.AddressType: return net.IP([]byte(dAVP.Data.(datatype.Address))), nil case datatype.DiameterIdentityType: return string(dAVP.Data.(datatype.DiameterIdentity)), nil case datatype.DiameterURIType: return string(dAVP.Data.(datatype.DiameterURI)), nil case datatype.EnumeratedType: return int32(dAVP.Data.(datatype.Enumerated)), nil case datatype.Float32Type: return float32(dAVP.Data.(datatype.Float32)), nil case datatype.Float64Type: return float64(dAVP.Data.(datatype.Float64)), nil case datatype.IPFilterRuleType: return string(dAVP.Data.(datatype.IPFilterRule)), nil case datatype.IPv4Type: return net.IP([]byte(dAVP.Data.(datatype.IPv4))), nil case datatype.Integer32Type: return int32(dAVP.Data.(datatype.Integer32)), nil case datatype.Integer64Type: return int64(dAVP.Data.(datatype.Integer64)), nil case datatype.OctetStringType: return string(dAVP.Data.(datatype.OctetString)), nil case datatype.QoSFilterRuleType: return string(dAVP.Data.(datatype.QoSFilterRule)), nil case datatype.TimeType: return time.Time(dAVP.Data.(datatype.Time)), nil case datatype.UTF8StringType: return string(dAVP.Data.(datatype.UTF8String)), nil case datatype.Unsigned32Type: return uint32(dAVP.Data.(datatype.Unsigned32)), nil case datatype.Unsigned64Type: return uint64(dAVP.Data.(datatype.Unsigned64)), nil } } func diamAVPAsString(dAVP *diam.AVP) (s string, err error) { var iface interface{} if iface, err = diamAVPAsIface(dAVP); err != nil { return } return utils.IfaceAsString(iface), nil } // newDiamDataType constructs dataType from valStr func newDiamDataType(typ datatype.TypeID, valStr, tmz string) (dt datatype.Type, err error) { switch typ { default: return nil, fmt.Errorf("unsupported AVP data type: %d", typ) case datatype.AddressType: return datatype.Address(net.ParseIP(valStr)), nil case datatype.DiameterIdentityType: return datatype.DiameterIdentity(valStr), nil case datatype.DiameterURIType: return datatype.DiameterURI(valStr), nil case datatype.EnumeratedType: i, err := strconv.ParseInt(valStr, 10, 32) if err != nil { return nil, err } return datatype.Enumerated(int32(i)), nil case datatype.Float32Type: f, err := strconv.ParseFloat(valStr, 32) if err != nil { return nil, err } return datatype.Float32(float32(f)), nil case datatype.Float64Type: f, err := strconv.ParseFloat(valStr, 64) if err != nil { return nil, err } return datatype.Float64(f), nil case datatype.IPFilterRuleType: return datatype.IPFilterRule(valStr), nil case datatype.IPv4Type: return datatype.IPv4(net.ParseIP(valStr)), nil case datatype.Integer32Type: i, err := strconv.ParseInt(valStr, 10, 32) if err != nil { return nil, err } return datatype.Integer32(int32(i)), nil case datatype.Integer64Type: i, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return nil, err } return datatype.Integer64(i), nil case datatype.OctetStringType: return datatype.OctetString(valStr), nil case datatype.QoSFilterRuleType: return datatype.QoSFilterRule(valStr), nil case datatype.TimeType: t, err := utils.ParseTimeDetectLayout(valStr, tmz) if err != nil { return nil, err } return datatype.Time(t), nil case datatype.UTF8StringType: return datatype.UTF8String(valStr), nil case datatype.Unsigned32Type: i, err := strconv.ParseUint(valStr, 10, 32) if err != nil { return nil, err } return datatype.Unsigned32(uint32(i)), nil case datatype.Unsigned64Type: i, err := strconv.ParseUint(valStr, 10, 64) if err != nil { return nil, err } return datatype.Unsigned64(i), nil } } func headerLen(a *diam.AVP) int { if a.Flags&avp.Vbit == avp.Vbit { return 12 } return 8 } func updateAVPLenght(avps []*diam.AVP) (l int) { for _, avp := range avps { if v, ok := (avp.Data).(*diam.GroupedAVP); ok { avp.Length = headerLen(avp) + updateAVPLenght(v.AVP) } l += avp.Length } return } // messageAddAVPsWithPath will dynamically add AVPs into the message // append: append to the message, on false overwrite if AVP is single or add to group if AVP is Grouped func messageSetAVPsWithPath(m *diam.Message, pathStr []string, avpValStr string, newBranch bool, tmz string) (err error) { if len(pathStr) == 0 { return errors.New("empty path as AVP filter") } path := utils.SliceStringToIface(pathStr) dictAVPs := make([]*dict.AVP, len(path)) // for each subpath, one dictionary AVP for i, subpath := range path { if dictAVP, err := m.Dictionary().FindAVP(m.Header.ApplicationID, subpath); err != nil { return err } else if dictAVP == nil { return fmt.Errorf("cannot find AVP with id: %s", path[len(path)-1]) } else { dictAVPs[i] = dictAVP } } lastAVPIdx := len(path) - 1 if dictAVPs[lastAVPIdx].Data.Type == diam.GroupedAVPType { return errors.New("last AVP in path cannot be GroupedAVP") } var msgAVP *diam.AVP // Keep a reference here towards last AVP for i := lastAVPIdx; i >= 0; i-- { var typeVal datatype.Type if i == lastAVPIdx { if typeVal, err = newDiamDataType(dictAVPs[i].Data.Type, avpValStr, tmz); err != nil { return err } } else { typeVal = &diam.GroupedAVP{ AVP: []*diam.AVP{msgAVP}} } msgAVP = diam.NewAVP(dictAVPs[i].Code, avp.Mbit, dictAVPs[i].VendorID, typeVal) // FixMe: maybe Mbit with dictionary one if i > 0 && !newBranch { avps, err := m.FindAVPsWithPath(path[:i], dict.UndefinedVendorID) if err != nil { return err } if len(avps) != 0 { // Group AVP already in the message prevGrpData, ok := avps[len(avps)-1].Data.(*diam.GroupedAVP) // Take the last avp found to append there if ok { prevGrpData.AVP = append(prevGrpData.AVP, msgAVP) m.Header.MessageLength += uint32(msgAVP.Len()) // updateAVPLenght(m.AVP) return nil } } } } if !newBranch { // Not group AVP, replace the previous set one with this one avps, err := m.FindAVPsWithPath(path, dict.UndefinedVendorID) if err != nil { return err } if len(avps) != 0 { // Group AVP already in the message m.Header.MessageLength -= uint32(avps[len(avps)-1].Len()) // decrease message length since we overwrite *avps[len(avps)-1] = *msgAVP m.Header.MessageLength += uint32(msgAVP.Len()) // updateAVPLenght(m.AVP) return nil } } m.AVP = append(m.AVP, msgAVP) m.Header.MessageLength += uint32(msgAVP.Len()) // updateAVPLenght(m.AVP) return nil } // writeOnConn writes the message on connection, logs failures func writeOnConn(c diam.Conn, m *diam.Message) (err error) { if _, err = m.WriteTo(c); err != nil { utils.Logger.Warning(fmt.Sprintf("<%s> failed writing message to %s, err: %s, msg: %s", utils.DiameterAgent, c.RemoteAddr(), err.Error(), m)) } return } // newDADataProvider constructs a DataProvider for a diameter message func newDADataProvider(c diam.Conn, m *diam.Message) config.DataProvider { return &diameterDP{c: c, m: m, cache: config.NewNavigableMap(nil)} } // diameterDP implements engine.DataProvider, serving as diam.Message data decoder // decoded data is only searched once and cached type diameterDP struct { c diam.Conn m *diam.Message cache *config.NavigableMap } // String is part of engine.DataProvider interface // when called, it will display the already parsed values out of cache func (dP *diameterDP) String() string { return utils.ToJSON(dP.m) } // AsNavigableMap is part of engine.DataProvider interface func (dP *diameterDP) AsNavigableMap([]*config.FCTemplate) ( nm *config.NavigableMap, err error) { return nil, utils.ErrNotImplemented } // FieldAsString is part of engine.DataProvider interface func (dP *diameterDP) FieldAsString(fldPath []string) (data string, err error) { var valIface interface{} valIface, err = dP.FieldAsInterface(fldPath) if err != nil { return } return utils.IfaceAsString(valIface), nil } // RemoteHost is part of engine.DataProvider interface func (dP *diameterDP) RemoteHost() net.Addr { return utils.NewNetAddr(dP.c.RemoteAddr().Network(), dP.c.RemoteAddr().String()) } // FieldAsInterface is part of engine.DataProvider interface func (dP *diameterDP) FieldAsInterface(fldPath []string) (data interface{}, err error) { if data, err = dP.cache.FieldAsInterface(fldPath); err != nil { if err != utils.ErrNotFound { // item found in cache return nil, err } err = nil // cancel previous err } else { return // data was found in cache } // lastPath can contain selector inside lastPath := fldPath[len(fldPath)-1] var slctrStr string if splt := strings.Split(lastPath, "["); len(splt) != 1 { lastPath = splt[0] if splt[1][len(splt[1])-1:] != "]" { return nil, fmt.Errorf("filter rule <%s> needs to end in ]", splt[1]) } slctrStr = splt[1][:len(splt[1])-1] // also strip the last ] } pathIface := utils.SliceStringToIface(fldPath) if slctrStr != "" { // last path was having selector inside before pathIface[len(pathIface)-1] = lastPath // last path was changed } var avps []*diam.AVP if avps, err = dP.m.FindAVPsWithPath( pathIface, dict.UndefinedVendorID); err != nil { return nil, err } else if len(avps) == 0 { return nil, utils.ErrNotFound } slectedIdx := 0 // by default we select AVP[0] if slctrStr != "" { if slectedIdx, err = strconv.Atoi(slctrStr); err != nil { // not int, compile it as RSRParser selIndxs := make(map[int]int) // use it to find intersection of all matched filters slctrStrs := strings.Split(slctrStr, utils.PipeSep) for _, slctrStr := range slctrStrs { slctr, err := config.NewRSRParser(slctrStr, true) if err != nil { return nil, err } var fltrAVPs []*diam.AVP for i := len(pathIface) - 1; i > 0; i-- { pathIface[i] = slctr.AttrName() // search for AVPs which are having common path but different end element pathIface = pathIface[:i+1] if fltrAVPs, err = dP.m.FindAVPsWithPath(pathIface, dict.UndefinedVendorID); err != nil || len(fltrAVPs) != 0 { break // found AVPs or got error, go and compare } } if err != nil { return nil, err } else if len(fltrAVPs) == 0 || len(fltrAVPs) != len(avps) { return nil, utils.ErrNotFound } for k, fAVP := range fltrAVPs { if dataAVP, err := diamAVPAsIface(fAVP); err != nil { return nil, err } else if _, err := slctr.ParseValue(dataAVP); err != nil { if err != utils.ErrFilterNotPassingNoCaps { return nil, err } continue // filter not passing, not really error } else { selIndxs[k+1] += 1 // filter passing, index it with one higher to cover 0 } } } var oneMatches bool for k, matches := range selIndxs { if matches == len(slctrStrs) { // all filters in selection matching oneMatches = true slectedIdx = k - 1 // decrease it to reflect real index break } } if !oneMatches { return nil, utils.ErrFilterNotPassingNoCaps } } } if slectedIdx >= len(avps) { return nil, errors.New("avp index higher than number of AVPs") } if data, err = diamAVPAsIface(avps[slectedIdx]); err != nil { return nil, err } dP.cache.Set(fldPath, data, false, false) return } // updateDiamMsgFromNavMap will update the diameter message with items from navigable map func updateDiamMsgFromNavMap(m *diam.Message, navMp *config.NavigableMap, tmz string) (err error) { // write reply into message pathIdx := make(map[string]int) // group items for same path for _, val := range navMp.Values() { nmItms, isNMItems := val.([]*config.NMItem) if !isNMItems { return fmt.Errorf("cannot encode reply value: %s, err: not NMItems", utils.ToJSON(val)) } // find out the first itm which is not an attribute var itm *config.NMItem if len(nmItms) == 1 { itm = nmItms[0] } else { // only for groups for i, cfgItm := range nmItms { itmPath := strings.Join(cfgItm.Path, utils.NestingSep) if i == 0 { // path is common, increase it only once pathIdx[itmPath] += 1 } if i == pathIdx[itmPath]-1 { // revert from multiple items to only one per config path itm = cfgItm break } } } if itm == nil { continue // all attributes, not writable to diameter packet } var newBranch bool if itm.Config != nil && itm.Config.NewBranch { newBranch = true } if err = messageSetAVPsWithPath(m, itm.Path, utils.IfaceAsString(itm.Data), newBranch, tmz); err != nil { return fmt.Errorf("setting item with path: %+v got err: %s", itm.Path, err.Error()) } } return } // diamAnswer builds up the answer to be sent back to the client func diamAnswer(m *diam.Message, resCode uint32, errFlag bool, rply *config.NavigableMap, tmz string) (a *diam.Message, err error) { a = m.Answer(resCode) if errFlag { a.Header.CommandFlags = diam.ErrorFlag } if err = updateDiamMsgFromNavMap(a, rply, tmz); err != nil { return nil, err } return } // negDiamAnswer is used to return the negative answer we need previous to func diamErr(m *diam.Message, resCode uint32, reqVars map[string]interface{}, tpl []*config.FCTemplate, tnt, tmz string, filterS *engine.FilterS) (a *diam.Message, err error) { aReq := NewAgentRequest( newDADataProvider(nil, m), reqVars, config.NewNavigableMap(nil), config.NewNavigableMap(nil), nil, tnt, tmz, filterS) var rplyData *config.NavigableMap if rplyData, err = aReq.AsNavigableMap(tpl); err != nil { return } return diamAnswer(m, resCode, true, rplyData, tmz) } func diamBareErr(m *diam.Message, resCode uint32) (a *diam.Message) { a = m.Answer(resCode) a.Header.CommandFlags = diam.ErrorFlag return } func disectDiamListen(addrs string) (ipAddrs []net.IP) { ipPort := strings.Split(addrs, utils.InInFieldSep) if ipPort[0] == "" { return } ips := strings.Split(ipPort[0], utils.HDR_VAL_SEP) ipAddrs = make([]net.IP, len(ips)) for i, ip := range ips { ipAddrs[i] = net.ParseIP(ip) } return } // diamMessageData is cached when data is needed (ie. ) type diamMsgData struct { c diam.Conn m *diam.Message vars map[string]interface{} }