diff --git a/agents/librad.go b/agents/librad.go index 2b0cc3382..bcc941194 100644 --- a/agents/librad.go +++ b/agents/librad.go @@ -100,7 +100,7 @@ func radReplyAppendAttributes(reply *radigo.Packet, agReq *AgentRequest, if err != nil { return err } - if cfgFld.Path == MetaRadReplyCode { // Special case used to control the reply code of RADIUS reply + if strings.Split(cfgFld.Path, utils.NestingSep)[1] == MetaRadReplyCode { // Special case used to control the reply code of RADIUS reply if err = reply.SetCodeWithName(fmtOut); err != nil { return err } diff --git a/agents/librad_test.go b/agents/librad_test.go index 9a0952b1c..fa27ec21f 100644 --- a/agents/librad_test.go +++ b/agents/librad_test.go @@ -136,7 +136,7 @@ func TestRadFieldOutVal(t *testing.T) { func TestRadReplyAppendAttributes(t *testing.T) { rply := radigo.NewPacket(radigo.AccessRequest, 2, dictRad, coder, "CGRateS.org").Reply() rplyFlds := []*config.FCTemplate{ - {Tag: "ReplyCode", Path: MetaRadReplyCode, Type: utils.META_COMPOSED, + {Tag: "ReplyCode", Path: utils.MetaRep + utils.NestingSep + MetaRadReplyCode, Type: utils.META_COMPOSED, Value: config.NewRSRParsersMustCompile("~*cgrep.Attributes.RadReply", true, utils.INFIELD_SEP)}, {Tag: "Acct-Session-Time", Path: "*rep.Acct-Session-Time", Type: utils.META_COMPOSED, Value: config.NewRSRParsersMustCompile("~*cgrep.MaxUsage{*duration_seconds}", true, utils.INFIELD_SEP)}, diff --git a/agents/radagent_handlr_it_test.go b/agents/radagent_handlr_it_test.go new file mode 100644 index 000000000..7a44b728b --- /dev/null +++ b/agents/radagent_handlr_it_test.go @@ -0,0 +1,309 @@ +//go:build integration +// +build integration + +/* +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 ( + "crypto/rand" + "fmt" + "math/big" + "net/rpc" + "os" + "path" + "path/filepath" + "testing" + "time" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" + "github.com/cgrates/radigo" +) + +var ( + raHCfgPath string + raHCfg *config.CGRConfig + raHAuthClnt *radigo.Client + raHRPC *rpc.Client + + sTestsRadiusH = []func(t *testing.T){ + testRAHitRemoveFolders, + testRAHitInitCfg, + testRAHitResetDataDb, + testRAHitResetStorDb, + testRAHitStartEngine, + testRAHitApierRpcConn, + testRAHitTPFromFolder, + testRAHitEmptyValueHandling, + testRAHitStopCgrEngine, + testRAHitRemoveFolders, + } +) + +// Test start here +func TestRAHandlerPlaygroundit(t *testing.T) { + for _, stest := range sTestsRadiusH { + t.Run("raonfigDIR", stest) + } +} + +func testRAHitRemoveFolders(t *testing.T) { + if err := os.RemoveAll("/tmp/TestRAitEmptyValueHandling"); err != nil { + t.Error(err) + } + +} + +func testRAHitInitCfg(t *testing.T) { + + content := `{ + // CGRateS Configuration file + // + + "general": { + "log_level": 7, + }, + + + "listen": { + "rpc_json": ":2012", // RPC JSON listening address + "rpc_gob": ":2013", // RPC GOB listening address + "http": ":2080", // HTTP listening address + }, + + + "data_db": { + "db_type": "*internal", + }, + + + "stor_db": { + "db_type": "*internal", + }, + + "rals": { + "enabled": true, + }, + + "schedulers": { + "enabled": true, + }, + + "cdrs": { + "enabled": true, + "rals_conns": ["*internal"], + }, + + "resources": { + "enabled": true, + "store_interval": "-1", + }, + + "attributes": { + "enabled": true, + }, + + "suppliers": { + "enabled": true, + }, + + "chargers": { + "enabled": true, + }, + + "sessions": { + "enabled": true, + "attributes_conns": ["*localhost"], + "cdrs_conns": ["*localhost"], + "rals_conns": ["*localhost"], + "resources_conns": ["*localhost"], + "chargers_conns": ["*internal"], + "debit_interval": "10s", + }, + + "radius_agent": { + "enabled": true, + "sessions_conns": ["*localhost"], + "request_processors": [ + { + "id": "RadiusMandatoryFail", + "filters": ["*string:~*vars.*radReqType:*radAuth","*string:~*req.User-Name:10011"], + "flags": ["*log", "*auth", "*attributes"], + "request_fields":[ + {"tag": "UserName", "path": "*cgreq.RadUserName", "type": "*composed", + "value": "~*req.User-Name"}, + {"tag": "Password", "path": "*cgreq.RadPassword", "type": "*composed", + "value": "~*req.User-Password"}, + {"tag": "ReplyMessage", "path": "*cgreq.RadReplyMessage", "type": "*constant", + "value": "*attributes"}, + ], + "reply_fields":[ + {"tag": "Code", "path": "*rep.*radReplyCode", "filters": ["*notempty:~*cgrep.Error:"], + "type": "*constant", "value": "AccessReject"}, + // {"tag": "ReplyMessage", "path": "*rep.Reply-Message","filters": ["*notempty:~*cgrep.Error:"], + // "type": "*composed", "value": "~*cgrep.Error"}, + {"tag": "ReplyMessage", "path": "*rep.Reply-Message", + "type": "*composed", "value": "~*cgrep.Attributes.RadReplyMessage"}, + ], + }, + ], + }, + + + + "apiers": { + "enabled": true, + "scheduler_conns": ["*internal"], + }, + + + } + ` + + var folderNameSuffix *big.Int + folderNameSuffix, err = rand.Int(rand.Reader, big.NewInt(10000)) + if err != nil { + t.Fatalf("could not generate random number for folder name suffix, err: %s", err.Error()) + } + raHCfgPath = fmt.Sprintf("/tmp/config%d", folderNameSuffix) + err = os.MkdirAll(raHCfgPath, 0755) + if err != nil { + t.Fatal(err) + } + filePath := filepath.Join(raHCfgPath, "cgrates.json") + err = os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + t.Fatal(err) + } + raHCfg, err = config.NewCGRConfigFromPath(raHCfgPath) + if err != nil { + t.Error(err) + } + + raHCfg.DataFolderPath = raHCfgPath // Share DataFolderPath through config towards StoreDb for Flush() + config.SetCgrConfig(raHCfg) +} + +// Remove data in both rating and accounting db +func testRAHitResetDataDb(t *testing.T) { + if err := engine.InitDataDb(raHCfg); err != nil { + t.Fatal(err) + } +} + +// Wipe out the cdr database +func testRAHitResetStorDb(t *testing.T) { + if err := engine.InitStorDb(raHCfg); err != nil { + t.Fatal(err) + } +} + +// Start CGR Engine +func testRAHitStartEngine(t *testing.T) { + if _, err := engine.StartEngine(raHCfgPath, *waitRater); err != nil { + t.Fatal(err) + } +} + +// Connect rpc client to rater +func testRAHitApierRpcConn(t *testing.T) { + var err error + raHRPC, err = newRPCClient(raHCfg.ListenCfg()) // We connect over JSON so we can also troubleshoot if needed + if err != nil { + t.Fatal(err) + } +} + +// Load the tariff plan, creating accounts and their balances +func testRAHitTPFromFolder(t *testing.T) { + writeFile := func(fileName, data string) error { + err := os.MkdirAll("/tmp/TestRAitEmptyValueHandling", os.ModePerm) + if err != nil { + t.Fatal(err) + } + csvFile, err := os.Create(path.Join("/tmp/TestRAitEmptyValueHandling", fileName)) + if err != nil { + return err + } + defer csvFile.Close() + _, err = csvFile.WriteString(data) + if err != nil { + return err + + } + return csvFile.Sync() + } + + // Create and populate Attributes.csv + if err := writeFile(utils.AttributesCsv, ` +#Tenant,ID,Contexts,FilterIDs,ActivationInterval,AttributeFilterIDs,Path,Type,Value,Blocker,Weight +cgrates.org,ATTR_RAD,*any,*string:~*req.RadUserName:10011;*prefix:~*req.RadPassword:CGRateSPassword3,,,,,,false,10 +#cgrates.org,ATTR_RAD,,,,,*req.RadReplyMessage,*constant,Access Accept,, +`); err != nil { + t.Fatal(err) + } + + var reply string + attrs := &utils.AttrLoadTpFromFolder{FolderPath: "/tmp/TestRAitEmptyValueHandling"} + if err := raHRPC.Call(utils.APIerSv1LoadTariffPlanFromFolder, attrs, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Error("Unexpected reply returned", reply) + } + time.Sleep(500 * time.Millisecond) +} + +func testRAHitEmptyValueHandling(t *testing.T) { + if raHAuthClnt, err = radigo.NewClient("udp", "127.0.0.1:1812", "CGRateS.org", dictRad, 1, nil); err != nil { + t.Fatal(err) + } + authReq := raHAuthClnt.NewRequest(radigo.AccessRequest, 1) // emulates Kamailio packet out of radius_load_caller_avps() + if err := authReq.AddAVPWithName("User-Name", "10011", ""); err != nil { + t.Error(err) + } + if err := authReq.AddAVPWithName("User-Password", "CGRateSPassword3", ""); err != nil { + t.Error(err) + } + // encode the password as required so we can decode it properly + authReq.AVPs[1].RawValue = radigo.EncodeUserPassword([]byte("CGRateSPassword3"), []byte("CGRateS.org"), authReq.Authenticator[:]) + reply, err := raHAuthClnt.SendRequest(authReq) + if err != nil { + t.Fatal(err) + } + if reply.Code != radigo.AccessReject { + t.Errorf("Received reply: %+v", reply) + } + exp := "ATTRIBUTES_ERROR:" + utils.MandatoryIEMissingCaps + ": [RadReplyMessage]" + if len(reply.AVPs) != 1 { // make sure max duration is received + t.Errorf("Received AVPs: %+v", reply.AVPs) + } else if exp != string(reply.AVPs[0].RawValue) { + t.Errorf("Expected <%+v>, Received: <%+v>", exp, string(reply.AVPs[0].RawValue)) + } +} + +func testRAHitStopCgrEngine(t *testing.T) { + if err := engine.KillEngine(100); err != nil { + t.Error(err) + } + if err := os.RemoveAll("/tmp/" + raHCfgPath); err != nil { + t.Error(err) + } +}