diff --git a/apier/cdre.go b/apier/cdre.go index 18e5e5285..0eb8e1c3c 100644 --- a/apier/cdre.go +++ b/apier/cdre.go @@ -57,6 +57,14 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E if roundDecimals == 0 { roundDecimals = self.Config.RoundingDecimals } + maskDestId := attr.MaskDestinationId + if len(maskDestId) == 0 { + maskDestId = self.Config.CdreMaskDestId + } + maskLen := attr.MaskLength + if maskLen == 0 { + maskLen = self.Config.CdreMaskLength + } cdrs, err := self.CdrDb.GetStoredCdrs(attr.CgrIds, attr.MediationRunId, attr.CdrHost, attr.CdrSource, attr.ReqType, attr.Direction, attr.Tenant, attr.Tor, attr.Account, attr.Subject, attr.DestinationPrefix, tStart, tEnd, attr.SkipErrors, attr.SkipRated) if err != nil { @@ -124,7 +132,7 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E return err } defer fileOut.Close() - fww, _ := cdre.NewFWCdrWriter(self.LogDb, fileOut, exportTemplate, exportId, roundDecimals) + fww, _ := cdre.NewFWCdrWriter(self.LogDb, fileOut, exportTemplate, exportId, roundDecimals, maskDestId, maskLen) exportedIds := make([]string, 0) unexportedIds := make(map[string]string) for _, cdr := range cdrs { diff --git a/apier/tutfsjson_local_test.go b/apier/tutfsjson_local_test.go index 099de9b75..8de50a86d 100644 --- a/apier/tutfsjson_local_test.go +++ b/apier/tutfsjson_local_test.go @@ -315,8 +315,8 @@ func TestMaxCallDuration(t *testing.T) { Subject: "1001", Account: "1001", Destination: "1002", - TimeStart: time.Now(), - TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration), + TimeStart: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC), + TimeEnd: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC).Add(fsjsonCfg.SMMaxCallDuration), } var remainingDurationFloat float64 if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil { @@ -334,8 +334,8 @@ func TestMaxCallDuration(t *testing.T) { Subject: "1002", Account: "1002", Destination: "1001", - TimeStart: time.Now(), - TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration), + TimeStart: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC), + TimeEnd: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC).Add(fsjsonCfg.SMMaxCallDuration), } if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil { t.Error(err) @@ -352,8 +352,8 @@ func TestMaxCallDuration(t *testing.T) { Subject: "1006", Account: "1006", Destination: "1001", - TimeStart: time.Now(), - TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration), + TimeStart: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC), + TimeEnd: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC).Add(fsjsonCfg.SMMaxCallDuration), } if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil { t.Error(err) @@ -371,8 +371,8 @@ func TestMaxCallDuration(t *testing.T) { Subject: "1007", Account: "1007", Destination: "1001", - TimeStart: time.Now(), - TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration), + TimeStart: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC), + TimeEnd: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC).Add(fsjsonCfg.SMMaxCallDuration), } if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil { t.Error(err) @@ -397,8 +397,8 @@ func TestMaxDebit1001(t *testing.T) { Subject: "1001", Account: "1001", Destination: "1002", - TimeStart: time.Now(), - TimeEnd: time.Now().Add(time.Duration(10) * time.Second), + TimeStart: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC), + TimeEnd: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC).Add(time.Duration(10) * time.Second), } if err := rater.Call("Responder.MaxDebit", cd, cc); err != nil { t.Error(err.Error()) @@ -436,8 +436,8 @@ func TestMaxDebit1007(t *testing.T) { Subject: "1007", Account: "1007", Destination: "1002", - TimeStart: time.Now(), - TimeEnd: time.Now().Add(time.Duration(10) * time.Second), + TimeStart: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC), + TimeEnd: time.Date(2014, 3, 27, 10, 42, 26, 0, time.UTC).Add(time.Duration(10) * time.Second), } if err := rater.Call("Responder.MaxDebit", cd, cc); err != nil { t.Error(err.Error()) diff --git a/cdre/fixedwidth.go b/cdre/fixedwidth.go index be3e30ffa..5ee748586 100644 --- a/cdre/fixedwidth.go +++ b/cdre/fixedwidth.go @@ -46,17 +46,21 @@ const ( META_NRCDRS = "cdrs_number" META_DURCDRS = "cdrs_duration" META_COSTCDRS = "cdrs_cost" + META_MASKDESTINATION = "mask_destination" ) var err error -func NewFWCdrWriter(logDb engine.LogStorage, outFile *os.File, exportTpl *config.CgrXmlCdreFwCfg, exportId string, roundDecimals int) (*FixedWidthCdrWriter, error) { +func NewFWCdrWriter(logDb engine.LogStorage, outFile *os.File, exportTpl *config.CgrXmlCdreFwCfg, exportId string, + roundDecimals int, maskDestId string, maskLen int) (*FixedWidthCdrWriter, error) { return &FixedWidthCdrWriter{ logDb: logDb, writer: outFile, exportTemplate: exportTpl, exportId: exportId, roundDecimals: roundDecimals, + maskDestId: maskDestId, + maskLen: maskLen, header: &bytes.Buffer{}, content: &bytes.Buffer{}, trailer: &bytes.Buffer{}}, nil @@ -68,6 +72,8 @@ type FixedWidthCdrWriter struct { exportTemplate *config.CgrXmlCdreFwCfg exportId string // Unique identifier or this export roundDecimals int + maskDestId string + maskLen int header, content, trailer *bytes.Buffer firstCdrATime, lastCdrATime time.Time numberOfRecords int @@ -77,8 +83,8 @@ type FixedWidthCdrWriter struct { // Return Json marshaled callCost attached to // Keep it separately so we test only this part in local tests -func (fww *FixedWidthCdrWriter) getCdrCostDetails(cgrId, runId string) (string, error) { - cc, err := fww.logDb.GetCallCostLog(cgrId, "", runId) +func (fwv *FixedWidthCdrWriter) getCdrCostDetails(cgrId, runId string) (string, error) { + cc, err := fwv.logDb.GetCallCostLog(cgrId, "", runId) if err != nil { return "", err } else if cc == nil { @@ -88,8 +94,15 @@ func (fww *FixedWidthCdrWriter) getCdrCostDetails(cgrId, runId string) (string, return string(ccJson), nil } +func (fwv *FixedWidthCdrWriter) maskedDestination(destination string) bool { + if len(fwv.maskDestId) != 0 && engine.CachedDestHasPrefix(fwv.maskDestId, destination) { + return true + } + return false +} + // Extracts the value specified by cfgHdr out of cdr -func (fww *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layout string) (string, error) { +func (fwv *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layout string) (string, error) { rsrField, err := utils.NewRSRField(cfgHdr) if err != nil { return "", err @@ -99,37 +112,47 @@ func (fww *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layo var cdrVal string switch rsrField.Id { case COST_DETAILS: // Special case when we need to further extract cost_details out of logDb - if cdrVal, err = fww.getCdrCostDetails(cdr.CgrId, cdr.MediationRunId); err != nil { + if cdrVal, err = fwv.getCdrCostDetails(cdr.CgrId, cdr.MediationRunId); err != nil { return "", err } case utils.COST: - cdrVal = cdr.FormatCost(fww.roundDecimals) + cdrVal = cdr.FormatCost(fwv.roundDecimals) case utils.SETUP_TIME: cdrVal = cdr.SetupTime.Format(layout) case utils.ANSWER_TIME: // Format time based on layout cdrVal = cdr.AnswerTime.Format(layout) + case utils.DESTINATION: + cdrVal = cdr.ExportFieldValue(utils.DESTINATION) + if fwv.maskLen != -1 && fwv.maskedDestination(cdrVal) { + cdrVal = MaskDestination(cdrVal, fwv.maskLen) + } default: cdrVal = cdr.ExportFieldValue(rsrField.Id) } return rsrField.ParseValue(cdrVal), nil } -func (fww *FixedWidthCdrWriter) metaHandler(tag, layout string) (string, error) { +func (fwv *FixedWidthCdrWriter) metaHandler(tag, arg string) (string, error) { switch tag { case META_EXPORTID: - return fww.exportId, nil + return fwv.exportId, nil case META_TIMENOW: - return time.Now().Format(layout), nil + return time.Now().Format(arg), nil case META_FIRSTCDRATIME: - return fww.firstCdrATime.Format(layout), nil + return fwv.firstCdrATime.Format(arg), nil case META_LASTCDRATIME: - return fww.lastCdrATime.Format(layout), nil + return fwv.lastCdrATime.Format(arg), nil case META_NRCDRS: - return strconv.Itoa(fww.numberOfRecords), nil + return strconv.Itoa(fwv.numberOfRecords), nil case META_DURCDRS: - return strconv.FormatFloat(fww.totalDuration.Seconds(), 'f', -1, 64), nil + return strconv.FormatFloat(fwv.totalDuration.Seconds(), 'f', -1, 64), nil case META_COSTCDRS: - return strconv.FormatFloat(utils.Round(fww.totalCost, fww.roundDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil + return strconv.FormatFloat(utils.Round(fwv.totalCost, fwv.roundDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil + case META_MASKDESTINATION: + if fwv.maskedDestination(arg) { + return "1", nil + } + return "0", nil default: return "", fmt.Errorf("Unsupported METATAG: %s", tag) } @@ -137,9 +160,9 @@ func (fww *FixedWidthCdrWriter) metaHandler(tag, layout string) (string, error) } // Writes the header into it's buffer -func (fww *FixedWidthCdrWriter) ComposeHeader() error { +func (fwv *FixedWidthCdrWriter) ComposeHeader() error { header := "" - for _, cfgFld := range fww.exportTemplate.Header.Fields { + for _, cfgFld := range fwv.exportTemplate.Header.Fields { var outVal string switch cfgFld.Type { case FILLER: @@ -148,7 +171,7 @@ func (fww *FixedWidthCdrWriter) ComposeHeader() error { case CONSTANT: outVal = cfgFld.Value case METATAG: - outVal, err = fww.metaHandler(cfgFld.Value, cfgFld.Layout) + outVal, err = fwv.metaHandler(cfgFld.Value, cfgFld.Layout) default: return fmt.Errorf("Unsupported field type: %s", cfgFld.Type) } @@ -167,14 +190,14 @@ func (fww *FixedWidthCdrWriter) ComposeHeader() error { return nil } header += "\n" // Done with cdr, postpend new line char - fww.header.WriteString(header) + fwv.header.WriteString(header) return nil } // Writes the trailer into it's buffer -func (fww *FixedWidthCdrWriter) ComposeTrailer() error { +func (fwv *FixedWidthCdrWriter) ComposeTrailer() error { trailer := "" - for _, cfgFld := range fww.exportTemplate.Trailer.Fields { + for _, cfgFld := range fwv.exportTemplate.Trailer.Fields { var outVal string switch cfgFld.Type { case FILLER: @@ -183,7 +206,7 @@ func (fww *FixedWidthCdrWriter) ComposeTrailer() error { case CONSTANT: outVal = cfgFld.Value case METATAG: - outVal, err = fww.metaHandler(cfgFld.Value, cfgFld.Layout) + outVal, err = fwv.metaHandler(cfgFld.Value, cfgFld.Layout) default: return fmt.Errorf("Unsupported field type: %s", cfgFld.Type) } @@ -202,18 +225,18 @@ func (fww *FixedWidthCdrWriter) ComposeTrailer() error { return nil } trailer += "\n" // Done with cdr, postpend new line char - fww.trailer.WriteString(trailer) + fwv.trailer.WriteString(trailer) return nil } // Write individual cdr into content buffer, build stats -func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { +func (fwv *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { if cdr == nil || len(cdr.CgrId) == 0 { // We do not export empty CDRs return nil } var err error cdrRow := "" - for _, cfgFld := range fww.exportTemplate.Content.Fields { + for _, cfgFld := range fwv.exportTemplate.Content.Fields { var outVal string switch cfgFld.Type { case FILLER: @@ -222,15 +245,17 @@ func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { case CONSTANT: outVal = cfgFld.Value case CDRFIELD: - outVal, err = fww.cdrFieldValue(cdr, cfgFld.Value, cfgFld.Layout) + outVal, err = fwv.cdrFieldValue(cdr, cfgFld.Value, cfgFld.Layout) case CONCATENATED_CDRFIELD: for _, fld := range strings.Split(cfgFld.Value, ",") { - if fldOut, err := fww.cdrFieldValue(cdr, fld, cfgFld.Layout); err != nil { + if fldOut, err := fwv.cdrFieldValue(cdr, fld, cfgFld.Layout); err != nil { break // The error will be reported bellow } else { outVal += fldOut } } + case METATAG: + outVal, err = fwv.metaHandler(cfgFld.Value, cfgFld.Layout) } if err != nil { engine.Logger.Err(fmt.Sprintf(" Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId, err.Error())) @@ -247,29 +272,29 @@ func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { return nil } cdrRow += "\n" // Done with cdr, postpend new line char - fww.content.WriteString(cdrRow) + fwv.content.WriteString(cdrRow) // Done with writing content, compute stats here - if fww.firstCdrATime.IsZero() || cdr.AnswerTime.Before(fww.firstCdrATime) { - fww.firstCdrATime = cdr.AnswerTime + if fwv.firstCdrATime.IsZero() || cdr.AnswerTime.Before(fwv.firstCdrATime) { + fwv.firstCdrATime = cdr.AnswerTime } - if cdr.AnswerTime.After(fww.lastCdrATime) { - fww.lastCdrATime = cdr.AnswerTime + if cdr.AnswerTime.After(fwv.lastCdrATime) { + fwv.lastCdrATime = cdr.AnswerTime } - fww.numberOfRecords += 1 - fww.totalDuration += cdr.Duration - fww.totalCost += cdr.Cost - fww.totalCost = utils.Round(fww.totalCost, fww.roundDecimals, utils.ROUNDING_MIDDLE) + fwv.numberOfRecords += 1 + fwv.totalDuration += cdr.Duration + fwv.totalCost += cdr.Cost + fwv.totalCost = utils.Round(fwv.totalCost, fwv.roundDecimals, utils.ROUNDING_MIDDLE) return nil } -func (fww *FixedWidthCdrWriter) Close() { - if fww.exportTemplate.Header != nil { - fww.ComposeHeader() +func (fwv *FixedWidthCdrWriter) Close() { + if fwv.exportTemplate.Header != nil { + fwv.ComposeHeader() } - if fww.exportTemplate.Trailer != nil { - fww.ComposeTrailer() + if fwv.exportTemplate.Trailer != nil { + fwv.ComposeTrailer() } - for _, buf := range []*bytes.Buffer{fww.header, fww.content, fww.trailer} { - fww.writer.Write(buf.Bytes()) + for _, buf := range []*bytes.Buffer{fwv.header, fwv.content, fwv.trailer} { + fwv.writer.Write(buf.Bytes()) } } diff --git a/cdre/fixedwidth_test.go b/cdre/fixedwidth_test.go index cf5a39e88..291b28e8d 100644 --- a/cdre/fixedwidth_test.go +++ b/cdre/fixedwidth_test.go @@ -58,7 +58,7 @@ var contentCfgFlds = []*config.CgrXmlCfgCdrField{ &config.CgrXmlCfgCdrField{Name: "Filler", Type: FILLER, Width: 8}, &config.CgrXmlCfgCdrField{Name: "TerminationCode", Type: CONCATENATED_CDRFIELD, Value: "operator,product", Width: 5, Strip: "right", Padding: "right"}, &config.CgrXmlCfgCdrField{Name: "Cost", Type: CDRFIELD, Value: utils.COST, Width: 9, Padding: "zeroleft"}, - &config.CgrXmlCfgCdrField{Name: "DestinationPrivacy", Type: CDRFIELD, Value: "destination_privacy", Width: 1, Strip: "right", Padding: "right"}, + &config.CgrXmlCfgCdrField{Name: "DestinationPrivacy", Type: METATAG, Value: META_MASKDESTINATION, Width: 1}, } var trailerCfgFlds = []*config.CgrXmlCfgCdrField{ @@ -91,7 +91,7 @@ func TestWriteCdr(t *testing.T) { if err := fwWriter.WriteCdr(cdr); err != nil { t.Error(err) } - eContentOut := "201001 1001 1002 0211 07111308420010 1 3dsafdsaf 0002.3457 \n" + eContentOut := "201001 1001 1002 0211 07111308420010 1 3dsafdsaf 0002.34570\n" contentOut := fwWriter.content.String() if len(contentOut) != 145 { t.Error("Unexpected content length", len(contentOut)) diff --git a/cdre/libfixedwidth.go b/cdre/libfixedwidth.go index ddbef8c60..38d507b7c 100644 --- a/cdre/libfixedwidth.go +++ b/cdre/libfixedwidth.go @@ -21,6 +21,7 @@ package cdre import ( "errors" "fmt" + "github.com/cgrates/cgrates/utils" ) // Used as generic function logic for various fields @@ -71,3 +72,18 @@ func FmtFieldWidth(source string, width int, strip, padding string, mandatory bo } return source, nil } + +// Mask a number of characters in the suffix of the destination +func MaskDestination(dest string, maskLen int) string { + destLen := len(dest) + if maskLen < 0 { + return dest + } else if maskLen > destLen { + maskLen = destLen + } + dest = dest[:destLen-maskLen] + for i := 0; i < maskLen; i++ { + dest += utils.MASK_CHAR + } + return dest +} diff --git a/cdre/libfixedwidth_test.go b/cdre/libfixedwidth_test.go index 7c932a302..4b0d8b555 100644 --- a/cdre/libfixedwidth_test.go +++ b/cdre/libfixedwidth_test.go @@ -114,3 +114,20 @@ func TestPaddingNotAllowed(t *testing.T) { t.Error("Expected error") } } + +func TestMaskDestination(t *testing.T) { + dest := "+4986517174963" + if destMasked := MaskDestination(dest, 3); destMasked != "+4986517174***" { + t.Error("Unexpected mask applied", destMasked) + } + if destMasked := MaskDestination(dest, -1); destMasked != dest { + t.Error("Negative maskLen should not modify destination", destMasked) + } + if destMasked := MaskDestination(dest, 0); destMasked != dest { + t.Error("Zero maskLen should not modify destination", destMasked) + } + if destMasked := MaskDestination(dest, 100); destMasked != "**************" { + t.Error("High maskLen should return complete mask", destMasked) + } + +} diff --git a/config/config.go b/config/config.go index c19f26953..bc88048c5 100644 --- a/config/config.go +++ b/config/config.go @@ -91,6 +91,8 @@ type CGRConfig struct { CDRSExtraFields []*utils.RSRField // Extra fields to store in CDRs CDRSMediator string // Address where to reach the Mediator. Empty for disabling mediation. <""|internal> CdreCdrFormat string // Format of the exported CDRs. + CdreMaskDestId string // Id of the destination list to be masked in CDRs + CdreMaskLength int // Number of digits to mask in the destination suffix if destination is in the MaskDestinationdsId CdreDir string // Path towards exported cdrs directory CdreExportedFields []*utils.RSRField // List of fields in the exported CDRs CdreFWXmlTemplate *CgrXmlCdreFwCfg // Use this configuration as export template in case of fixed fields length @@ -197,6 +199,8 @@ func (self *CGRConfig) setDefaults() error { self.CDRSExtraFields = []*utils.RSRField{} self.CDRSMediator = "" self.CdreCdrFormat = "csv" + self.CdreMaskDestId = "" + self.CdreMaskLength = 0 self.CdreDir = "/var/log/cgrates/cdr/cdre" self.CdrcEnabled = false self.CdrcCdrs = utils.INTERNAL @@ -488,6 +492,12 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) { if hasOpt = c.HasOption("cdre", "cdr_format"); hasOpt { cfg.CdreCdrFormat, _ = c.GetString("cdre", "cdr_format") } + if hasOpt = c.HasOption("cdre", "mask_destination_id"); hasOpt { + cfg.CdreMaskDestId, _ = c.GetString("cdre", "mask_destination_id") + } + if hasOpt = c.HasOption("cdre", "mask_length"); hasOpt { + cfg.CdreMaskLength, _ = c.GetInt("cdre", "mask_length") + } if hasOpt = c.HasOption("cdre", "export_template"); hasOpt { // Load configs for csv normally from template, fixed_width from xml file exportTemplate, _ := c.GetString("cdre", "export_template") if cfg.CdreCdrFormat != utils.CDRE_FIXED_WIDTH { // Csv most likely diff --git a/config/config_test.go b/config/config_test.go index 061d3772f..942d16c15 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -82,6 +82,8 @@ func TestDefaults(t *testing.T) { eCfg.CDRSExtraFields = []*utils.RSRField{} eCfg.CDRSMediator = "" eCfg.CdreCdrFormat = "csv" + eCfg.CdreMaskDestId = "" + eCfg.CdreMaskLength = 0 eCfg.CdreDir = "/var/log/cgrates/cdr/cdre" eCfg.CdrcEnabled = false eCfg.CdrcCdrs = utils.INTERNAL @@ -236,6 +238,8 @@ func TestConfigFromFile(t *testing.T) { eCfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "test"}} eCfg.CDRSMediator = "test" eCfg.CdreCdrFormat = "test" + eCfg.CdreMaskDestId = "test" + eCfg.CdreMaskLength = 99 eCfg.CdreExportedFields = []*utils.RSRField{&utils.RSRField{Id: "test"}} eCfg.CdreDir = "test" eCfg.CdrcEnabled = true diff --git a/config/test_data.txt b/config/test_data.txt index a68fa2c38..515e2d664 100644 --- a/config/test_data.txt +++ b/config/test_data.txt @@ -49,6 +49,8 @@ mediator = test # Address where to reach the Mediator. Empty for disabling me [cdre] cdr_format = test # Exported CDRs format +mask_destination_id = test # Destination id containing called addresses to be masked on export +mask_length = 99 # Length of the destination suffix to be masked export_dir = test # Path where the exported CDRs will be placed export_template = test # List of fields in the exported CDRs diff --git a/data/conf/cgrates.cfg b/data/conf/cgrates.cfg index c1670ac75..2af875892 100644 --- a/data/conf/cgrates.cfg +++ b/data/conf/cgrates.cfg @@ -52,6 +52,8 @@ [cdre] # cdr_format = csv # Exported CDRs format +# mask_destination_id = # Destination id containing called addresses to be masked on export +# mask_length = 0 # Length of the destination suffix to be masked # export_dir = /var/log/cgrates/cdr/cdrexport/csv # Path where the exported CDRs will be placed # export_template = cgrid,mediation_runid,accid,cdrhost,reqtype,direction,tenant,tor,account,subject,destination,setup_time,answer_time,duration,cost # Exported fields template <""|fld1,fld2|*xml:instance_name> diff --git a/engine/destinations.go b/engine/destinations.go index baf4e06fa..8d4e1df4c 100644 --- a/engine/destinations.go +++ b/engine/destinations.go @@ -20,6 +20,7 @@ package engine import ( "encoding/json" + "github.com/cgrates/cgrates/cache2go" "strings" "github.com/cgrates/cgrates/history" @@ -68,3 +69,15 @@ func (d *Destination) GetHistoryRecord() history.Record { Payload: js, } } + +// Reverse search in cache to see if prefix belongs to destination id +func CachedDestHasPrefix(destId, prefix string) bool { + if cached, err := cache2go.GetCached(DESTINATION_PREFIX + prefix); err == nil { + for _, cachedDstId := range cached.([]string) { + if destId == cachedDstId { + return true + } + } + } + return false +} diff --git a/engine/destinations_test.go b/engine/destinations_test.go index eefeec7a5..95cd1bd5e 100644 --- a/engine/destinations_test.go +++ b/engine/destinations_test.go @@ -101,6 +101,30 @@ func TestDestinationGetNotExistsCache(t *testing.T) { } } +func TestCachedDestHasPrefix(t *testing.T) { + if !CachedDestHasPrefix("NAT", "0256") { + t.Error("Could not find prefix in destination") + } +} + +func TestCachedDestHasWrongPrefix(t *testing.T) { + if CachedDestHasPrefix("NAT", "771") { + t.Error("Prefix should not belong to destination") + } +} + +func TestNonCachedDestRightPrefix(t *testing.T) { + if CachedDestHasPrefix("FAKE", "0256") { + t.Error("Destination should not belong to prefix") + } +} + +func TestNonCachedDestWrongPrefix(t *testing.T) { + if CachedDestHasPrefix("FAKE", "771") { + t.Error("Both arguments should be fake") + } +} + /********************************* Benchmarks **********************************/ func BenchmarkDestinationStorageStoreRestore(b *testing.B) { diff --git a/utils/apitpdata.go b/utils/apitpdata.go index 3f0593b3c..2b9f50ec4 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -321,6 +321,8 @@ type AttrExpFileCdrs struct { ExportFileName string // If provided the output filename will be set to this ExportTemplate string // Exported fields template <""|fld1,fld2|*xml:instance_name> RoundingDecimals int // Overwrite configured roundDecimals with this dynamically + MaskDestinationId string // Overwrite configured MaskDestId + MaskLength int // Overwrite configured MaskLength CgrIds []string // If provided, it will filter based on the cgrids present in list MediationRunId string // If provided, it will filter on mediation runid CdrHost string // If provided, it will filter cdrhost diff --git a/utils/consts.go b/utils/consts.go index caeec9633..ff0f9b8e6 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -91,9 +91,10 @@ const ( INTERNAL = "internal" ZERO_RATING_SUBJECT_PREFIX = "*zero" OK = "OK" - CDRE_FIXED_WIDTH = "fixed_width" + CDRE_FIXED_WIDTH = "fwv" XML_PROFILE_PREFIX = "*xml:" CDRE = "cdre" + MASK_CHAR = "*" ) var (