/* 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 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 */ package agents import ( "fmt" "slices" "strconv" "strings" "time" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/sessions" "github.com/cgrates/cgrates/utils" "github.com/cgrates/fsock" ) // ToDo: Introduce support for RSRFields const ( varPrefix = "variable_" // Freswitch event proprities names SUBJECT = varPrefix + utils.CGRSubject ACCOUNT = varPrefix + utils.CGRAccount DESTINATION = varPrefix + utils.CGRDestination REQTYPE = varPrefix + utils.CGRReqType //prepaid or postpaid CATEGORY = varPrefix + utils.CGRCategory VAR_CGR_ROUTE = varPrefix + utils.CGRRoute UUID = "Unique-ID" // -Unique ID for this call leg CSTMID = varPrefix + utils.CGRTenant CALL_DEST_NR = "Caller-Destination-Number" SIP_REQ_USER = "variable_sip_req_user" PARK_TIME = "Caller-Profile-Created-Time" SETUP_TIME = "Caller-Channel-Created-Time" ANSWER_TIME = "Caller-Channel-Answered-Time" END_TIME = "Caller-Channel-Hangup-Time" DURATION = "variable_billsec" NAME = "Event-Name" HEARTBEAT = "HEARTBEAT" ANSWER = "CHANNEL_ANSWER" HANGUP = "CHANNEL_HANGUP_COMPLETE" PARK = "CHANNEL_PARK" AUTH_OK = "AUTH_OK" DISCONNECT = "SWITCH DISCONNECT" MANAGER_REQUEST = "MANAGER_REQUEST" USERNAME = "Caller-Username" FS_IPv4 = "FreeSWITCH-IPv4" HANGUP_CAUSE = "Hangup-Cause" PDD_MEDIA_MS = "variable_progress_mediamsec" PDD_NOMEDIA_MS = "variable_progressmsec" IGNOREPARK = "variable_cgr_ignorepark" FS_VARPREFIX = "variable_" VarCGRFlags = varPrefix + utils.CGRFlags VarCGROpts = varPrefix + utils.CGROpts CGRResourceAllocation = "cgr_resource_allocation" VAR_CGR_DISCONNECT_CAUSE = varPrefix + utils.CGRDisconnectCause VAR_CGR_CMPUTELCR = varPrefix + utils.CGRComputeLCR FsConnID = "FsConnID" // used to share connID info in event for remote disconnects VarAnswerEpoch = "variable_answer_epoch" VarCGRACD = varPrefix + utils.CgrAcd VarCGROriginHost = varPrefix + utils.CGROriginHost VarCGRMaxUsage = "cgr_max_usage" ) func NewFSEvent(strEv string) (fsev FSEvent) { return fsock.FSEventStrToMap(strEv, nil) } // Event type holding a mapping of all event's proprieties type FSEvent map[string]string // Nice printing for the event object. func (fsev FSEvent) String() (result string) { for k, v := range fsev { result += fmt.Sprintf("%s = %s\n", k, v) } result += "==============================================================" return } func (fsev FSEvent) GetName() string { return fsev[NAME] } // Account calling func (fsev FSEvent) GetAccount(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[ACCOUNT], fsev[USERNAME]) } // Rating subject being charged func (fsev FSEvent) GetSubject(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[SUBJECT], fsev.GetAccount(fieldName)) } // Charging destination number func (fsev FSEvent) GetDestination(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[DESTINATION], fsev[CALL_DEST_NR], fsev[SIP_REQ_USER]) } // Original dialed destination number, useful in case of unpark func (fsev FSEvent) GetCallDestNr(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[CALL_DEST_NR], fsev[SIP_REQ_USER]) } func (fsev FSEvent) GetCategory(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[CATEGORY], config.CgrConfig().GeneralCfg().DefaultCategory) } func (fsev FSEvent) GetUUID() string { return fsev[UUID] } func (fsev FSEvent) GetSessionIds() []string { return []string{fsev.GetUUID()} } func (fsev FSEvent) GetTenant(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[CSTMID], config.CgrConfig().GeneralCfg().DefaultTenant) } func (fsev FSEvent) GetReqType(fieldName string) string { var reqTypeDetected = "" // Used to automatically disable processing of the request if fsev["variable_process_cdr"] == "false" { // FS will not generated CDR here reqTypeDetected = utils.MetaNone } else if fsev["Caller-Dialplan"] == "inline" { // Used for internally generated dialplan, eg refer coming from another box, not in our control reqTypeDetected = utils.MetaNone } if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[REQTYPE], reqTypeDetected, config.CgrConfig().GeneralCfg().DefaultReqType) } func (fsev FSEvent) MissingParameter(timezone string) string { if strings.TrimSpace(fsev.GetAccount(utils.MetaDefault)) == "" { return utils.AccountField } if strings.TrimSpace(fsev.GetSubject(utils.MetaDefault)) == "" { return utils.Subject } if strings.TrimSpace(fsev.GetDestination(utils.MetaDefault)) == "" { return utils.Destination } if strings.TrimSpace(fsev.GetCategory(utils.MetaDefault)) == "" { return utils.Category } if strings.TrimSpace(fsev.GetUUID()) == "" { return utils.OriginID } if strings.TrimSpace(fsev.GetTenant(utils.MetaDefault)) == "" { return utils.Tenant } if strings.TrimSpace(fsev.GetCallDestNr(utils.MetaDefault)) == "" { return CALL_DEST_NR } return "" } func (fsev FSEvent) GetSetupTime(fieldName, timezone string) (t time.Time, err error) { fsSTimeStr, hasKey := fsev[SETUP_TIME] if hasKey && fsSTimeStr != "0" { // Discard the nanoseconds information since MySQL cannot store them in early versions and csv uses default seconds so CGRID will not corelate fsSTimeStr = fsSTimeStr[:len(fsSTimeStr)-6] } sTimeStr := utils.FirstNonEmpty(fsev[fieldName], fsSTimeStr) if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value sTimeStr = fieldName[len(utils.StaticValuePrefix):] } return utils.ParseTimeDetectLayout(sTimeStr, timezone) } func (fsev FSEvent) GetAnswerTime(fieldName, timezone string) (t time.Time, err error) { fsATimeStr, hasKey := fsev[ANSWER_TIME] if hasKey && fsATimeStr != "0" { // Discard the nanoseconds information since MySQL cannot store them in early versions and csv uses default seconds so CGRID will not corelate fsATimeStr = fsATimeStr[:len(fsATimeStr)-6] } aTimeStr := utils.FirstNonEmpty(fsev[fieldName], fsATimeStr) if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value aTimeStr = fieldName[len(utils.StaticValuePrefix):] } return utils.ParseTimeDetectLayout(aTimeStr, timezone) } func (fsev FSEvent) GetEndTime(fieldName, timezone string) (t time.Time, err error) { return utils.ParseTimeDetectLayout(fsev[END_TIME], timezone) } func (fsev FSEvent) GetDuration(fieldName string) (time.Duration, error) { durStr := utils.FirstNonEmpty(fsev[fieldName], fsev[DURATION]) if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value durStr = fieldName[len(utils.StaticValuePrefix):] } return utils.ParseDurationWithSecs(durStr) } func (fsev FSEvent) GetPdd(fieldName string) (time.Duration, error) { var PDDStr string if slices.Contains([]string{utils.MetaDefault, utils.PDD}, fieldName) { PDDStr = utils.FirstNonEmpty(fsev[PDD_MEDIA_MS], fsev[PDD_NOMEDIA_MS]) if len(PDDStr) != 0 { PDDStr = PDDStr + "ms" // PDD is in milliseconds and CGR expects it in seconds } } else if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value PDDStr = fieldName[len(utils.StaticValuePrefix):] } else { PDDStr = fsev[fieldName] } return utils.ParseDurationWithSecs(PDDStr) } func (fsev FSEvent) GetADC(fieldName string) (time.Duration, error) { var ACDStr string if slices.Contains([]string{utils.MetaDefault, utils.ACD}, fieldName) { ACDStr = utils.FirstNonEmpty(fsev[VarCGRACD]) if len(ACDStr) != 0 { ACDStr = ACDStr + "s" // ACD is in seconds and CGR expects it in seconds } } else if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value ACDStr = fieldName[len(utils.StaticValuePrefix):] } else { ACDStr = fsev[fieldName] } return utils.ParseDurationWithSecs(ACDStr) } func (fsev FSEvent) GetRoute(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[VAR_CGR_ROUTE]) } func (fsev FSEvent) GetDisconnectCause(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[VAR_CGR_DISCONNECT_CAUSE], fsev[HANGUP_CAUSE]) } func (fsev FSEvent) GetOriginatorIP(fieldName string) string { if strings.HasPrefix(fieldName, utils.StaticValuePrefix) { // Static value return fieldName[len(utils.StaticValuePrefix):] } return utils.FirstNonEmpty(fsev[fieldName], fsev[VarCGROriginHost], fsev[FS_IPv4]) } // GetOriginHost returns the first non empty between: fsev[VarCGROriginHost], conns[connId].cfg.Alias and fsev[FS_IPv4] func (fsev FSEvent) GetOriginHost() string { return utils.FirstNonEmpty(fsev[VarCGROriginHost], fsev[FS_IPv4]) } func (fsev FSEvent) GetExtraFields() map[string]string { extraFields := make(map[string]string) const dynprefix string = utils.MetaDynReq + utils.NestingSep for _, fldRule := range config.CgrConfig().FsAgentCfg().ExtraFields { if !strings.HasPrefix(fldRule.Rules, dynprefix) { continue } attrName := fldRule.AttrName()[5:] if parsed, err := fsev.ParseEventValue(attrName, fldRule, config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil { utils.Logger.Warning(fmt.Sprintf("<%s> error: %s parsing event rule: %+v", utils.FreeSWITCHAgent, err.Error(), fldRule)) } else { extraFields[attrName] = parsed } } return extraFields } // Used in derived charging and sittuations when we need to run regexp on fields func (fsev FSEvent) ParseEventValue(attrName string, rsrFld *config.RSRParser, timezone string) (parsed string, err error) { switch attrName { case utils.ToR: return rsrFld.ParseValue(utils.MetaVoice) case utils.OriginID: return rsrFld.ParseValue(fsev.GetUUID()) case utils.OriginHost: return rsrFld.ParseValue(fsev.GetOriginHost()) case utils.Source: return rsrFld.ParseValue("FS_EVENT") case utils.RequestType: return rsrFld.ParseValue(fsev.GetReqType("")) case utils.Tenant: return rsrFld.ParseValue(fsev.GetTenant("")) case utils.Category: return rsrFld.ParseValue(fsev.GetCategory("")) case utils.AccountField: return rsrFld.ParseValue(fsev.GetAccount("")) case utils.Subject: return rsrFld.ParseValue(fsev.GetSubject("")) case utils.Destination: return rsrFld.ParseValue(fsev.GetDestination("")) case utils.SetupTime: st, _ := fsev.GetSetupTime("", timezone) return rsrFld.ParseValue(st.String()) case utils.AnswerTime: at, _ := fsev.GetAnswerTime("", timezone) return rsrFld.ParseValue(at.String()) case utils.Usage: dur, _ := fsev.GetDuration("") return rsrFld.ParseValue(strconv.FormatInt(dur.Nanoseconds(), 10)) case utils.PDD: PDD, _ := fsev.GetPdd(utils.MetaDefault) return rsrFld.ParseValue(strconv.FormatFloat(PDD.Seconds(), 'f', -1, 64)) case utils.Route: return rsrFld.ParseValue(fsev.GetRoute("")) case utils.DisconnectCause: return rsrFld.ParseValue(fsev.GetDisconnectCause("")) case utils.RunID: return rsrFld.ParseValue(utils.MetaDefault) case utils.Cost: return rsrFld.ParseValue(strconv.FormatFloat(-1, 'f', -1, 64)) // Recommended to use FormatCost default: if parsed, err = rsrFld.ParseValue(fsev[attrName]); err != nil { parsed, err = rsrFld.ParseValue(fsev[FS_VARPREFIX+attrName]) } return } } // AsCGREvent converts FSEvent into CGREvent func (fsev FSEvent) AsCGREvent(timezone string) (cgrEv *utils.CGREvent, err error) { sTime, err := fsev.GetSetupTime(utils.MetaDefault, timezone) if err != nil { return nil, err } cgrEv = &utils.CGREvent{ Tenant: fsev.GetTenant(utils.MetaDefault), ID: utils.UUIDSha1Prefix(), Time: &sTime, Event: fsev.AsMapStringInterface(timezone), APIOpts: fsev.GetOptions(), } return cgrEv, nil } // Used with RLs func (fsev FSEvent) AsMapStringInterface(timezone string) map[string]any { mp := make(map[string]any) for fld, val := range fsev.GetExtraFields() { mp[fld] = val } mp[utils.ToR] = utils.MetaVoice mp[utils.OriginID] = fsev.GetUUID() mp[utils.OriginHost] = fsev.GetOriginHost() mp[utils.Source] = "FS_" + fsev.GetName() mp[utils.RequestType] = fsev.GetReqType(utils.MetaDefault) mp[utils.Tenant] = fsev.GetTenant(utils.MetaDefault) mp[utils.Category] = fsev.GetCategory(utils.MetaDefault) mp[utils.AccountField] = fsev.GetAccount(utils.MetaDefault) mp[utils.Subject] = fsev.GetSubject(utils.MetaDefault) mp[utils.Destination] = fsev.GetDestination(utils.MetaDefault) mp[utils.SetupTime], _ = fsev.GetSetupTime(utils.MetaDefault, timezone) mp[utils.AnswerTime], _ = fsev.GetAnswerTime(utils.MetaDefault, timezone) mp[utils.Usage], _ = fsev.GetDuration(utils.MetaDefault) mp[utils.PDD], _ = fsev.GetPdd(utils.MetaDefault) mp[utils.ACD], _ = fsev.GetADC(utils.MetaDefault) mp[utils.Cost] = -1.0 mp[utils.Route] = fsev.GetRoute(utils.MetaDefault) mp[utils.DisconnectCause] = fsev.GetDisconnectCause(utils.MetaDefault) return mp } // V1AuthorizeArgs returns the arguments used in SMGv1.Authorize func (fsev FSEvent) V1AuthorizeArgs() (args *sessions.V1AuthorizeArgs) { cgrEv, err := fsev.AsCGREvent(config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { return } cgrEv.Event[utils.Usage] = config.CgrConfig().SessionSCfg().GetDefaultUsage(utils.IfaceAsString(cgrEv.Event[utils.ToR])) // no billsec available in auth args = &sessions.V1AuthorizeArgs{ // defaults CGREvent: cgrEv, } subsystems, has := fsev[VarCGRFlags] if !has { utils.Logger.Warning(fmt.Sprintf("<%s> cgr_flags variable is not set, using defaults", utils.FreeSWITCHAgent)) args.GetMaxUsage = true return } args.ParseFlags(subsystems, utils.InfieldSep) return } // V1InitSessionArgs returns the arguments used in SessionSv1.InitSession func (fsev FSEvent) V1InitSessionArgs() (args *sessions.V1InitSessionArgs) { cgrEv, err := fsev.AsCGREvent(config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { return } args = &sessions.V1InitSessionArgs{ // defaults CGREvent: cgrEv, } subsystems, has := fsev[VarCGRFlags] if !has { utils.Logger.Warning(fmt.Sprintf("<%s> cgr_flags variable is not set, using defaults", utils.FreeSWITCHAgent)) args.InitSession = true return } args.ParseFlags(subsystems, utils.InfieldSep) return } // V1TerminateSessionArgs returns the arguments used in SMGv1.TerminateSession func (fsev FSEvent) V1TerminateSessionArgs() (args *sessions.V1TerminateSessionArgs) { cgrEv, err := fsev.AsCGREvent(config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { return } args = &sessions.V1TerminateSessionArgs{ // defaults CGREvent: cgrEv, } subsystems, has := fsev[VarCGRFlags] if !has { utils.Logger.Warning(fmt.Sprintf("<%s> cgr_flags variable is not set, using defaults", utils.FreeSWITCHAgent)) args.TerminateSession = true return } args.ParseFlags(subsystems, utils.InfieldSep) return } // SliceAsFsArray Converts a slice of strings into a FS array string, contains len(array) at first index since FS does not support len(ARRAY::) for now func SliceAsFsArray(slc []string) (arry string) { if len(slc) == 0 { return } arry = fmt.Sprintf("ARRAY::%d", len(slc)) for _, itm := range slc { arry += "|:" + itm } return } // GetOptions returns the possible options func (fsev FSEvent) GetOptions() (mp map[string]any) { mp = make(map[string]any) opts, has := fsev[VarCGROpts] if !has { return } for _, opt := range strings.Split(opts, utils.FieldsSep) { spltOpt := strings.SplitN(opt, utils.InInFieldSep, 2) if len(spltOpt) != 2 { continue } mp[spltOpt[0]] = spltOpt[1] } return }