mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-13 02:56:24 +05:00
add *3gpp_uli user location converter
Parses the 3GPP-User-Location-Info AVP into structured location data allowing field access via an optional path (e.g. *3gpp_uli:TAI.MCC).
This commit is contained in:
committed by
Dan Christian Bogos
parent
be08b1d07b
commit
4c64f4f876
10
docs/rsr.rst
10
docs/rsr.rst
@@ -128,6 +128,16 @@ Converters transform the extracted value. Chain them with ``&``::
|
||||
* ``*e164Domain`` - extract domain from NAPTR record
|
||||
* ``*ip2hex`` - IP to hex
|
||||
* ``*sipuri_host``, ``*sipuri_user``, ``*sipuri_method`` - parse SIP URIs
|
||||
* ``*3gpp_uli`` - decode 3GPP-User-Location-Info hex to ULI object
|
||||
* ``*3gpp_uli:path`` - extract specific field from ULI
|
||||
|
||||
ULI component paths: ``CGI``, ``SAI``, ``RAI``, ``TAI``, ``ECGI``, ``TAI5GS``, ``NCGI``
|
||||
|
||||
Field paths: ``TAI.MCC``, ``TAI.MNC``, ``TAI.TAC``, ``ECGI.MCC``, ``ECGI.MNC``, ``ECGI.ECI``, ``NCGI.NCI``, etc.
|
||||
|
||||
Example::
|
||||
|
||||
~*req.3GPP-User-Location-Info{*3gpp_uli:TAI.MCC}
|
||||
|
||||
**Time**
|
||||
|
||||
|
||||
164
general_tests/diam_uli_it_test.go
Normal file
164
general_tests/diam_uli_it_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
//go: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 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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package general_tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/agents"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"github.com/cgrates/go-diameter/diam"
|
||||
"github.com/cgrates/go-diameter/diam/avp"
|
||||
"github.com/cgrates/go-diameter/diam/datatype"
|
||||
)
|
||||
|
||||
func TestDiamULI(t *testing.T) {
|
||||
// t.Skip("configuration reference for *3gpp_uli request_fields; does not assert values")
|
||||
switch *utils.DBType {
|
||||
case utils.MetaInternal:
|
||||
case utils.MetaMySQL, utils.MetaMongo, utils.MetaPostgres:
|
||||
t.SkipNow()
|
||||
default:
|
||||
t.Fatal("unsupported dbtype value")
|
||||
}
|
||||
|
||||
cfgJSON := `{
|
||||
"sessions": {
|
||||
"enabled": true
|
||||
},
|
||||
"diameter_agent": {
|
||||
"enabled": true,
|
||||
"request_processors": [
|
||||
{
|
||||
"id": "tgpp_loc_info",
|
||||
"flags": [
|
||||
"*dryrun"
|
||||
],
|
||||
"request_fields": [
|
||||
{
|
||||
"tag": "ULI",
|
||||
"path": "*cgreq.ULI",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info"
|
||||
},
|
||||
{
|
||||
"tag": "DecodedULI",
|
||||
"path": "*cgreq.DecodedULI",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli}"
|
||||
},
|
||||
{
|
||||
"tag": "TAI",
|
||||
"path": "*cgreq.TAI",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:TAI}"
|
||||
},
|
||||
{
|
||||
"tag": "TAI-MCC",
|
||||
"path": "*cgreq.TAI-MCC",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:TAI.MCC}"
|
||||
},
|
||||
{
|
||||
"tag": "TAI-MNC",
|
||||
"path": "*cgreq.TAI-MNC",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:TAI.MNC}"
|
||||
},
|
||||
{
|
||||
"tag": "TAI-TAC",
|
||||
"path": "*cgreq.TAI-TAC",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:TAI.TAC}"
|
||||
},
|
||||
{
|
||||
"tag": "ECGI",
|
||||
"path": "*cgreq.ECGI",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:ECGI}"
|
||||
},
|
||||
{
|
||||
"tag": "ECGI-MCC",
|
||||
"path": "*cgreq.ECGI-MCC",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:ECGI.MCC}"
|
||||
},
|
||||
{
|
||||
"tag": "ECGI-MNC",
|
||||
"path": "*cgreq.ECGI-MNC",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:ECGI.MNC}"
|
||||
},
|
||||
{
|
||||
"tag": "ECGI-ECI",
|
||||
"path": "*cgreq.ECGI-ECI",
|
||||
"type": "*variable",
|
||||
"value": "~*req.Service-Information.PS-Information.3GPP-User-Location-Info{*3gpp_uli:ECGI.ECI}"
|
||||
}
|
||||
],
|
||||
"reply_fields": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
ng := engine.TestEngine{
|
||||
ConfigJSON: cfgJSON,
|
||||
DBCfg: engine.InternalDBCfg,
|
||||
LogBuffer: &bytes.Buffer{},
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
t.Log(ng.LogBuffer)
|
||||
})
|
||||
_, cfg := ng.Run(t)
|
||||
|
||||
diamClient, err := agents.NewDiameterClient(cfg.DiameterAgentCfg().Listeners[0].Address, "localhost",
|
||||
cfg.DiameterAgentCfg().OriginRealm, cfg.DiameterAgentCfg().VendorID,
|
||||
cfg.DiameterAgentCfg().ProductName, utils.DiameterFirmwareRevision,
|
||||
cfg.DiameterAgentCfg().DictionariesPath, cfg.DiameterAgentCfg().Listeners[0].Network)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
userLocInfoRaw := "8245f750000145f75000000101"
|
||||
ccr := diam.NewRequest(diam.CreditControl, 4, nil)
|
||||
ccr.NewAVP(avp.ServiceInformation, avp.Mbit, 10415,
|
||||
&diam.GroupedAVP{
|
||||
AVP: []*diam.AVP{
|
||||
diam.NewAVP(avp.PSInformation, avp.Mbit, 10415,
|
||||
&diam.GroupedAVP{
|
||||
AVP: []*diam.AVP{
|
||||
diam.NewAVP(avp.TGPPUserLocationInfo, avp.Mbit, 10415, datatype.OctetString(userLocInfoRaw)),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if err := diamClient.SendMessage(ccr); err != nil {
|
||||
t.Errorf("failed to send diameter message: %v", err)
|
||||
}
|
||||
_ = diamClient.ReceivedMessage(2 * time.Second)
|
||||
}
|
||||
@@ -1331,6 +1331,7 @@ const (
|
||||
MetaDurationNanoseconds = "*duration_nanoseconds"
|
||||
MetaDurationMinutes = "*duration_minutes"
|
||||
MetaGigawords = "*gigawords"
|
||||
Meta3GPPULI = "*3gpp_uli"
|
||||
CapAttributes = "Attributes"
|
||||
CapResourceAllocation = "ResourceAllocation"
|
||||
CapAllocatedIP = "AllocatedIP"
|
||||
|
||||
@@ -136,6 +136,8 @@ func NewDataConverter(params string) (conv DataConverter, err error) {
|
||||
return ConnStatusConverter{}, nil
|
||||
case strings.HasPrefix(params, MetaGigawords):
|
||||
return new(GigawordsConverter), nil
|
||||
case strings.HasPrefix(params, Meta3GPPULI):
|
||||
return NewULIConverter(params)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported converter definition: <%s>", params)
|
||||
}
|
||||
@@ -859,3 +861,40 @@ func (c ConnStatusConverter) Convert(in any) (any, error) {
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported connection status: %q", status)
|
||||
}
|
||||
|
||||
// ULIConverter decodes 3GPP-User-Location-Info and extracts fields by path.
|
||||
type ULIConverter struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// NewULIConverter creates a ULI converter. The path after the colon specifies
|
||||
// which field to extract (e.g. "*3gpp_uli:TAI.MCC"). Empty path returns the
|
||||
// full ULI object.
|
||||
func NewULIConverter(params string) (*ULIConverter, error) {
|
||||
_, path, _ := strings.Cut(params, InInFieldSep)
|
||||
return &ULIConverter{path: path}, nil
|
||||
}
|
||||
|
||||
// Convert implements DataConverter interface.
|
||||
func (c *ULIConverter) Convert(in any) (any, error) {
|
||||
raw := IfaceAsString(in)
|
||||
if raw == "" {
|
||||
return nil, errors.New("empty ULI input")
|
||||
}
|
||||
|
||||
data, err := hex.DecodeString(strings.TrimPrefix(raw, "0x"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ULI hex: %w", err)
|
||||
}
|
||||
|
||||
uli, err := DecodeULI(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.path == "" {
|
||||
return uli, nil
|
||||
}
|
||||
|
||||
return uli.GetField(c.path)
|
||||
}
|
||||
|
||||
386
utils/uli.go
Normal file
386
utils/uli.go
Normal file
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Geographic location types per 3GPP TS 29.061 section 16.4.7.2
|
||||
const (
|
||||
ULITypeCGI = 0 // Cell Global Identity (2G)
|
||||
ULITypeSAI = 1 // Service Area Identity (3G)
|
||||
ULITypeRAI = 2 // Routing Area Identity (2G/3G)
|
||||
ULITypeTAI = 128 // Tracking Area Identity (4G)
|
||||
ULITypeECGI = 129 // E-UTRAN Cell Global Identifier (4G)
|
||||
ULITypeTAIECGI = 130 // TAI and ECGI (4G)
|
||||
ULITypeNCGI = 135 // NR Cell Global Identifier (5G)
|
||||
ULIType5GSTAI = 136 // 5G Tracking Area Identity
|
||||
ULIType5GSTAINCGI = 137 // 5GS TAI and NCGI (5G)
|
||||
)
|
||||
|
||||
// ULI holds decoded 3GPP-User-Location-Info.
|
||||
type ULI struct {
|
||||
CGI *CGI `json:"CGI,omitempty"`
|
||||
SAI *SAI `json:"SAI,omitempty"`
|
||||
RAI *RAI `json:"RAI,omitempty"`
|
||||
TAI *TAI `json:"TAI,omitempty"`
|
||||
ECGI *ECGI `json:"ECGI,omitempty"`
|
||||
TAI5GS *TAI5GS `json:"TAI5GS,omitempty"`
|
||||
NCGI *NCGI `json:"NCGI,omitempty"`
|
||||
}
|
||||
|
||||
// CGI is Cell Global Identity (2G GSM).
|
||||
type CGI struct {
|
||||
MCC string `json:"MCC"`
|
||||
MNC string `json:"MNC"`
|
||||
LAC uint16 `json:"LAC"`
|
||||
CI uint16 `json:"CI"`
|
||||
}
|
||||
|
||||
// SAI is Service Area Identity (3G UMTS).
|
||||
type SAI struct {
|
||||
MCC string `json:"MCC"`
|
||||
MNC string `json:"MNC"`
|
||||
LAC uint16 `json:"LAC"`
|
||||
SAC uint16 `json:"SAC"`
|
||||
}
|
||||
|
||||
// RAI is Routing Area Identity (2G/3G).
|
||||
type RAI struct {
|
||||
MCC string `json:"MCC"`
|
||||
MNC string `json:"MNC"`
|
||||
LAC uint16 `json:"LAC"`
|
||||
RAC uint8 `json:"RAC"`
|
||||
}
|
||||
|
||||
// TAI is Tracking Area Identity (4G LTE).
|
||||
type TAI struct {
|
||||
MCC string `json:"MCC"`
|
||||
MNC string `json:"MNC"`
|
||||
TAC uint16 `json:"TAC"`
|
||||
}
|
||||
|
||||
// ECGI is E-UTRAN Cell Global Identifier (4G LTE).
|
||||
type ECGI struct {
|
||||
MCC string `json:"MCC"`
|
||||
MNC string `json:"MNC"`
|
||||
ECI uint32 `json:"ECI"`
|
||||
}
|
||||
|
||||
// TAI5GS is 5G Tracking Area Identity.
|
||||
type TAI5GS struct {
|
||||
MCC string `json:"MCC"`
|
||||
MNC string `json:"MNC"`
|
||||
TAC uint32 `json:"TAC"` // 24-bit (vs 16-bit in 4G TAI)
|
||||
}
|
||||
|
||||
// NCGI is NR Cell Global Identifier (5G).
|
||||
type NCGI struct {
|
||||
MCC string `json:"MCC"`
|
||||
MNC string `json:"MNC"`
|
||||
NCI uint64 `json:"NCI"` // 36-bit NR Cell Identity
|
||||
}
|
||||
|
||||
// DecodeULI parses 3GPP-User-Location-Info from bytes per 3GPP TS 29.061 section 16.4.7.2.
|
||||
func DecodeULI(data []byte) (*ULI, error) {
|
||||
if len(data) < 2 {
|
||||
return nil, errors.New("ULI data too short")
|
||||
}
|
||||
|
||||
uli := &ULI{}
|
||||
locType := data[0]
|
||||
pos := 1
|
||||
|
||||
switch locType {
|
||||
case ULITypeCGI:
|
||||
if len(data) < 8 { // 1 type + 3 PLMN + 2 LAC + 2 CI
|
||||
return nil, errors.New("insufficient data for CGI")
|
||||
}
|
||||
uli.CGI = decodeCGI(data[pos:])
|
||||
|
||||
case ULITypeSAI:
|
||||
if len(data) < 8 { // 1 type + 3 PLMN + 2 LAC + 2 SAC
|
||||
return nil, errors.New("insufficient data for SAI")
|
||||
}
|
||||
uli.SAI = decodeSAI(data[pos:])
|
||||
|
||||
case ULITypeRAI:
|
||||
if len(data) < 7 { // 1 type + 3 PLMN + 2 LAC + 1 RAC
|
||||
return nil, errors.New("insufficient data for RAI")
|
||||
}
|
||||
uli.RAI = decodeRAI(data[pos:])
|
||||
|
||||
case ULITypeTAI:
|
||||
if len(data) < 6 { // 1 type + 3 PLMN + 2 TAC
|
||||
return nil, errors.New("insufficient data for TAI")
|
||||
}
|
||||
uli.TAI = decodeTAI(data[pos:])
|
||||
|
||||
case ULITypeECGI:
|
||||
if len(data) < 8 { // 1 type + 3 PLMN + 4 ECI
|
||||
return nil, errors.New("insufficient data for ECGI")
|
||||
}
|
||||
uli.ECGI = decodeECGI(data[pos:])
|
||||
|
||||
case ULITypeTAIECGI:
|
||||
if len(data) < 13 { // 1 type + 5 TAI + 7 ECGI
|
||||
return nil, errors.New("insufficient data for TAI+ECGI")
|
||||
}
|
||||
uli.TAI = decodeTAI(data[pos:])
|
||||
uli.ECGI = decodeECGI(data[pos+5:])
|
||||
|
||||
case ULITypeNCGI:
|
||||
if len(data) < 9 { // 1 type + 8 NCGI
|
||||
return nil, errors.New("insufficient data for NCGI")
|
||||
}
|
||||
uli.NCGI = decodeNCGI(data[pos:])
|
||||
|
||||
case ULIType5GSTAI:
|
||||
if len(data) < 7 { // 1 type + 6 5GS TAI
|
||||
return nil, errors.New("insufficient data for 5GS TAI")
|
||||
}
|
||||
uli.TAI5GS = decodeTAI5GS(data[pos:])
|
||||
|
||||
case ULIType5GSTAINCGI:
|
||||
if len(data) < 15 { // 1 type + 6 5GS TAI + 8 NCGI
|
||||
return nil, errors.New("insufficient data for 5GS TAI+NCGI")
|
||||
}
|
||||
uli.TAI5GS = decodeTAI5GS(data[pos:])
|
||||
uli.NCGI = decodeNCGI(data[pos+6:])
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported ULI location type: %d", locType)
|
||||
}
|
||||
|
||||
return uli, nil
|
||||
}
|
||||
|
||||
// decodePLMN extracts MCC and MNC from 3 bytes (TS 24.008 section 10.5.1.13).
|
||||
// Each digit is one nibble: [MCC2|MCC1] [MNC3|MCC3] [MNC2|MNC1].
|
||||
// MNC3=0xF means the MNC is only 2 digits.
|
||||
func decodePLMN(data []byte) (mcc, mnc string) {
|
||||
mcc1 := data[0] & 0x0F
|
||||
mcc2 := data[0] >> 4
|
||||
mcc3 := data[1] & 0x0F
|
||||
mnc3 := data[1] >> 4
|
||||
mnc1 := data[2] & 0x0F
|
||||
mnc2 := data[2] >> 4
|
||||
|
||||
mcc = fmt.Sprintf("%d%d%d", mcc1, mcc2, mcc3)
|
||||
|
||||
if mnc3 == 0x0F {
|
||||
mnc = fmt.Sprintf("%d%d", mnc1, mnc2)
|
||||
} else {
|
||||
mnc = fmt.Sprintf("%d%d%d", mnc1, mnc2, mnc3)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func decodeCGI(data []byte) *CGI {
|
||||
// PLMN + LAC + CI (TS 29.274 section 8.21.1)
|
||||
mcc, mnc := decodePLMN(data)
|
||||
return &CGI{
|
||||
MCC: mcc,
|
||||
MNC: mnc,
|
||||
LAC: binary.BigEndian.Uint16(data[3:5]),
|
||||
CI: binary.BigEndian.Uint16(data[5:7]),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeSAI(data []byte) *SAI {
|
||||
// PLMN + LAC + SAC (TS 29.274 section 8.21.2)
|
||||
mcc, mnc := decodePLMN(data)
|
||||
return &SAI{
|
||||
MCC: mcc,
|
||||
MNC: mnc,
|
||||
LAC: binary.BigEndian.Uint16(data[3:5]),
|
||||
SAC: binary.BigEndian.Uint16(data[5:7]),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRAI(data []byte) *RAI {
|
||||
// PLMN + LAC + RAC (TS 29.274 section 8.21.3)
|
||||
mcc, mnc := decodePLMN(data)
|
||||
return &RAI{
|
||||
MCC: mcc,
|
||||
MNC: mnc,
|
||||
LAC: binary.BigEndian.Uint16(data[3:5]),
|
||||
RAC: data[5],
|
||||
}
|
||||
}
|
||||
|
||||
func decodeTAI(data []byte) *TAI {
|
||||
// PLMN + TAC (TS 29.274 section 8.21.4)
|
||||
mcc, mnc := decodePLMN(data)
|
||||
return &TAI{
|
||||
MCC: mcc,
|
||||
MNC: mnc,
|
||||
TAC: binary.BigEndian.Uint16(data[3:5]),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeECGI(data []byte) *ECGI {
|
||||
mcc, mnc := decodePLMN(data)
|
||||
// the leading 4 bits are spare (TS 29.274 section 8.21.5)
|
||||
eci := binary.BigEndian.Uint32(data[3:7]) & 0x0FFFFFFF
|
||||
return &ECGI{
|
||||
MCC: mcc,
|
||||
MNC: mnc,
|
||||
ECI: eci,
|
||||
}
|
||||
}
|
||||
|
||||
func decodeTAI5GS(data []byte) *TAI5GS {
|
||||
mcc, mnc := decodePLMN(data)
|
||||
// TAC is 24 bits in 5GS (TS 38.413 section 9.3.3.11), unlike 16 bits in 4G TAI
|
||||
tac := uint32(data[3])<<16 | uint32(data[4])<<8 | uint32(data[5])
|
||||
return &TAI5GS{
|
||||
MCC: mcc,
|
||||
MNC: mnc,
|
||||
TAC: tac,
|
||||
}
|
||||
}
|
||||
|
||||
func decodeNCGI(data []byte) *NCGI {
|
||||
mcc, mnc := decodePLMN(data)
|
||||
// the leading 4 bits are spare (TS 38.413 section 9.3.1.7)
|
||||
// TODO: check why Wireshark's ULI dissector uses trail-spare for NCGI
|
||||
nci := uint64(data[3]&0x0F)<<32 |
|
||||
uint64(data[4])<<24 |
|
||||
uint64(data[5])<<16 |
|
||||
uint64(data[6])<<8 |
|
||||
uint64(data[7])
|
||||
return &NCGI{
|
||||
MCC: mcc,
|
||||
MNC: mnc,
|
||||
NCI: nci,
|
||||
}
|
||||
}
|
||||
|
||||
// GetField retrieves a value found at the specified path (e.g. "TAI.MCC" or "ECGI.ECI").
|
||||
func (uli *ULI) GetField(path string) (any, error) {
|
||||
parts := strings.SplitN(path, ".", 2)
|
||||
if len(parts) == 0 || parts[0] == "" {
|
||||
return nil, errors.New("empty path")
|
||||
}
|
||||
|
||||
var loc any
|
||||
var mcc, mnc string
|
||||
|
||||
switch parts[0] {
|
||||
case "CGI":
|
||||
if uli.CGI == nil {
|
||||
return nil, errors.New("CGI not present in ULI")
|
||||
}
|
||||
loc, mcc, mnc = uli.CGI, uli.CGI.MCC, uli.CGI.MNC
|
||||
case "SAI":
|
||||
if uli.SAI == nil {
|
||||
return nil, errors.New("SAI not present in ULI")
|
||||
}
|
||||
loc, mcc, mnc = uli.SAI, uli.SAI.MCC, uli.SAI.MNC
|
||||
case "RAI":
|
||||
if uli.RAI == nil {
|
||||
return nil, errors.New("RAI not present in ULI")
|
||||
}
|
||||
loc, mcc, mnc = uli.RAI, uli.RAI.MCC, uli.RAI.MNC
|
||||
case "TAI":
|
||||
if uli.TAI == nil {
|
||||
return nil, errors.New("TAI not present in ULI")
|
||||
}
|
||||
loc, mcc, mnc = uli.TAI, uli.TAI.MCC, uli.TAI.MNC
|
||||
case "ECGI":
|
||||
if uli.ECGI == nil {
|
||||
return nil, errors.New("ECGI not present in ULI")
|
||||
}
|
||||
loc, mcc, mnc = uli.ECGI, uli.ECGI.MCC, uli.ECGI.MNC
|
||||
case "TAI5GS":
|
||||
if uli.TAI5GS == nil {
|
||||
return nil, errors.New("TAI5GS not present in ULI")
|
||||
}
|
||||
loc, mcc, mnc = uli.TAI5GS, uli.TAI5GS.MCC, uli.TAI5GS.MNC
|
||||
case "NCGI":
|
||||
if uli.NCGI == nil {
|
||||
return nil, errors.New("NCGI not present in ULI")
|
||||
}
|
||||
loc, mcc, mnc = uli.NCGI, uli.NCGI.MCC, uli.NCGI.MNC
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown ULI component: %s", parts[0])
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
return loc, nil
|
||||
}
|
||||
|
||||
return uliFieldValue(loc, parts[1], mcc, mnc)
|
||||
}
|
||||
|
||||
func uliFieldValue(loc any, field, mcc, mnc string) (any, error) {
|
||||
switch field {
|
||||
case "MCC":
|
||||
return mcc, nil
|
||||
case "MNC":
|
||||
return mnc, nil
|
||||
}
|
||||
|
||||
switch l := loc.(type) {
|
||||
case *CGI:
|
||||
switch field {
|
||||
case "LAC":
|
||||
return l.LAC, nil
|
||||
case "CI":
|
||||
return l.CI, nil
|
||||
}
|
||||
case *SAI:
|
||||
switch field {
|
||||
case "LAC":
|
||||
return l.LAC, nil
|
||||
case "SAC":
|
||||
return l.SAC, nil
|
||||
}
|
||||
case *RAI:
|
||||
switch field {
|
||||
case "LAC":
|
||||
return l.LAC, nil
|
||||
case "RAC":
|
||||
return l.RAC, nil
|
||||
}
|
||||
case *TAI:
|
||||
if field == "TAC" {
|
||||
return l.TAC, nil
|
||||
}
|
||||
case *ECGI:
|
||||
if field == "ECI" {
|
||||
return l.ECI, nil
|
||||
}
|
||||
case *TAI5GS:
|
||||
if field == "TAC" {
|
||||
return l.TAC, nil
|
||||
}
|
||||
case *NCGI:
|
||||
if field == "NCI" {
|
||||
return l.NCI, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown field: %s", field)
|
||||
}
|
||||
429
utils/uli_test.go
Normal file
429
utils/uli_test.go
Normal file
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodePLMN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hex string
|
||||
expectedMCC string
|
||||
expectedMNC string
|
||||
}{
|
||||
{
|
||||
name: "2-digit MNC (547/05)",
|
||||
hex: "45f750",
|
||||
expectedMCC: "547",
|
||||
expectedMNC: "05",
|
||||
},
|
||||
{
|
||||
name: "3-digit MNC (310/260)",
|
||||
hex: "130062",
|
||||
expectedMCC: "310",
|
||||
expectedMNC: "260",
|
||||
},
|
||||
{
|
||||
name: "2-digit MNC (262/01)",
|
||||
hex: "62f210",
|
||||
expectedMCC: "262",
|
||||
expectedMNC: "01",
|
||||
},
|
||||
{
|
||||
name: "3GPP test PLMN 2-digit MNC (001/01)",
|
||||
hex: "00f110",
|
||||
expectedMCC: "001",
|
||||
expectedMNC: "01",
|
||||
},
|
||||
{
|
||||
name: "3GPP test PLMN 3-digit MNC (001/001)",
|
||||
hex: "001100",
|
||||
expectedMCC: "001",
|
||||
expectedMNC: "001",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := hex.DecodeString(tt.hex)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid test hex: %v", err)
|
||||
}
|
||||
mcc, mnc := decodePLMN(data)
|
||||
if mcc != tt.expectedMCC {
|
||||
t.Errorf("MCC: got %q, want %q", mcc, tt.expectedMCC)
|
||||
}
|
||||
if mnc != tt.expectedMNC {
|
||||
t.Errorf("MNC: got %q, want %q", mnc, tt.expectedMNC)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeULI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hex string
|
||||
expected *ULI
|
||||
}{
|
||||
{
|
||||
name: "CGI",
|
||||
hex: "0062f21012345678",
|
||||
expected: &ULI{
|
||||
CGI: &CGI{MCC: "262", MNC: "01", LAC: 0x1234, CI: 0x5678},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SAI",
|
||||
hex: "0162f2101234abcd",
|
||||
expected: &ULI{
|
||||
SAI: &SAI{MCC: "262", MNC: "01", LAC: 0x1234, SAC: 0xABCD},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "RAI",
|
||||
hex: "0262f210123456",
|
||||
expected: &ULI{
|
||||
RAI: &RAI{MCC: "262", MNC: "01", LAC: 0x1234, RAC: 0x56},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TAI",
|
||||
hex: "8013006204d2",
|
||||
expected: &ULI{
|
||||
TAI: &TAI{MCC: "310", MNC: "260", TAC: 1234},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ECGI",
|
||||
hex: "8145f75000000101",
|
||||
expected: &ULI{
|
||||
ECGI: &ECGI{MCC: "547", MNC: "05", ECI: 257},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TAI+ECGI",
|
||||
hex: "8245f750000145f75000000101",
|
||||
expected: &ULI{
|
||||
TAI: &TAI{MCC: "547", MNC: "05", TAC: 1},
|
||||
ECGI: &ECGI{MCC: "547", MNC: "05", ECI: 257},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NCGI",
|
||||
hex: "871300620123456789",
|
||||
expected: &ULI{
|
||||
NCGI: &NCGI{MCC: "310", MNC: "260", NCI: 0x123456789},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "5GS TAI",
|
||||
hex: "88130062123456",
|
||||
expected: &ULI{
|
||||
TAI5GS: &TAI5GS{MCC: "310", MNC: "260", TAC: 0x123456},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "5GS TAI+NCGI",
|
||||
hex: "891300620000011300620000000101",
|
||||
expected: &ULI{
|
||||
TAI5GS: &TAI5GS{MCC: "310", MNC: "260", TAC: 1},
|
||||
NCGI: &NCGI{MCC: "310", MNC: "260", NCI: 257},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := hex.DecodeString(tt.hex)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid test hex: %v", err)
|
||||
}
|
||||
got, err := DecodeULI(data)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeULI failed: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.expected) {
|
||||
t.Errorf("got %s, want %s", ToJSON(got), ToJSON(tt.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestULI_GetField(t *testing.T) {
|
||||
uli := &ULI{
|
||||
TAI: &TAI{
|
||||
MCC: "547",
|
||||
MNC: "05",
|
||||
TAC: 1,
|
||||
},
|
||||
ECGI: &ECGI{
|
||||
MCC: "547",
|
||||
MNC: "05",
|
||||
ECI: 257,
|
||||
},
|
||||
TAI5GS: &TAI5GS{
|
||||
MCC: "310",
|
||||
MNC: "260",
|
||||
TAC: 0x123456,
|
||||
},
|
||||
NCGI: &NCGI{
|
||||
MCC: "310",
|
||||
MNC: "260",
|
||||
NCI: 0x123456789,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
expected any
|
||||
}{
|
||||
{"TAI", uli.TAI},
|
||||
{"TAI.MCC", "547"},
|
||||
{"TAI.MNC", "05"},
|
||||
{"TAI.TAC", uint16(1)},
|
||||
{"ECGI", uli.ECGI},
|
||||
{"ECGI.MCC", "547"},
|
||||
{"ECGI.MNC", "05"},
|
||||
{"ECGI.ECI", uint32(257)},
|
||||
{"TAI5GS", uli.TAI5GS},
|
||||
{"TAI5GS.MCC", "310"},
|
||||
{"TAI5GS.MNC", "260"},
|
||||
{"TAI5GS.TAC", uint32(0x123456)},
|
||||
{"NCGI", uli.NCGI},
|
||||
{"NCGI.MCC", "310"},
|
||||
{"NCGI.MNC", "260"},
|
||||
{"NCGI.NCI", uint64(0x123456789)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
got, err := uli.GetField(tt.path)
|
||||
if err != nil {
|
||||
t.Fatalf("GetField(%q) error: %v", tt.path, err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.expected) {
|
||||
t.Errorf("GetField(%q): got %v (%T), want %v (%T)",
|
||||
tt.path, got, got, tt.expected, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestULI_GetField_Errors(t *testing.T) {
|
||||
uli := &ULI{
|
||||
TAI: &TAI{MCC: "547", MNC: "05", TAC: 1},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{"missing ECGI", "ECGI"},
|
||||
{"missing CGI", "CGI"},
|
||||
{"missing TAI5GS", "TAI5GS"},
|
||||
{"missing NCGI", "NCGI"},
|
||||
{"invalid field", "TAI.INVALID"},
|
||||
{"invalid component", "INVALID"},
|
||||
{"empty path", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := uli.GetField(tt.path)
|
||||
if err == nil {
|
||||
t.Errorf("GetField(%q) should have returned error", tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestULIConverter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params string
|
||||
input string
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "Extract TAI.MCC",
|
||||
params: "*3gpp_uli:TAI.MCC",
|
||||
input: "8245f750000145f75000000101",
|
||||
expected: "547",
|
||||
},
|
||||
{
|
||||
name: "Extract TAI.MNC",
|
||||
params: "*3gpp_uli:TAI.MNC",
|
||||
input: "8245f750000145f75000000101",
|
||||
expected: "05",
|
||||
},
|
||||
{
|
||||
name: "Extract TAI.TAC",
|
||||
params: "*3gpp_uli:TAI.TAC",
|
||||
input: "8245f750000145f75000000101",
|
||||
expected: uint16(1),
|
||||
},
|
||||
{
|
||||
name: "Extract ECGI.ECI",
|
||||
params: "*3gpp_uli:ECGI.ECI",
|
||||
input: "8245f750000145f75000000101",
|
||||
expected: uint32(257),
|
||||
},
|
||||
{
|
||||
name: "Extract TAI5GS.MCC from 5GS TAI",
|
||||
params: "*3gpp_uli:TAI5GS.MCC",
|
||||
input: "88130062123456",
|
||||
expected: "310",
|
||||
},
|
||||
{
|
||||
name: "Extract TAI5GS.TAC from 5GS TAI",
|
||||
params: "*3gpp_uli:TAI5GS.TAC",
|
||||
input: "88130062123456",
|
||||
expected: uint32(0x123456),
|
||||
},
|
||||
{
|
||||
name: "Extract NCGI.NCI from NCGI",
|
||||
params: "*3gpp_uli:NCGI.NCI",
|
||||
input: "871300620123456789",
|
||||
expected: uint64(0x123456789),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
conv, err := NewULIConverter(tt.params)
|
||||
if err != nil {
|
||||
t.Fatalf("NewULIConverter failed: %v", err)
|
||||
}
|
||||
got, err := conv.Convert(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("Convert failed: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.expected) {
|
||||
t.Errorf("got %v (%T), want %v (%T)",
|
||||
got, got, tt.expected, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeULI_InsufficientData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hex string
|
||||
}{
|
||||
{"CGI too short", "0062f210123456"},
|
||||
{"SAI too short", "0162f210123456"},
|
||||
{"RAI too short", "0262f2101234"},
|
||||
{"TAI too short", "8062f21012"},
|
||||
{"ECGI too short", "8162f210123456"},
|
||||
{"TAI+ECGI too short", "8262f2101234567890"},
|
||||
{"5GS TAI too short", "881300621234"},
|
||||
{"NCGI too short", "8713006201234567"},
|
||||
{"5GS TAI+NCGI too short", "8913006200000113006200000001"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := hex.DecodeString(tt.hex)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid test hex: %v", err)
|
||||
}
|
||||
_, err = DecodeULI(data)
|
||||
if err == nil {
|
||||
t.Error("DecodeULI should have returned error for insufficient data")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeULI_UnsupportedType(t *testing.T) {
|
||||
data, err := hex.DecodeString("0362f21012345678")
|
||||
if err != nil {
|
||||
t.Fatalf("invalid test hex: %v", err)
|
||||
}
|
||||
|
||||
_, err = DecodeULI(data)
|
||||
if err == nil {
|
||||
t.Error("DecodeULI should have returned error for unsupported type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestULIConverter_0xPrefix(t *testing.T) {
|
||||
conv, err := NewULIConverter("*3gpp_uli:TAI.MCC")
|
||||
if err != nil {
|
||||
t.Fatalf("NewULIConverter failed: %v", err)
|
||||
}
|
||||
got, err := conv.Convert("0x8245f750000145f75000000101")
|
||||
if err != nil {
|
||||
t.Fatalf("Convert failed: %v", err)
|
||||
}
|
||||
if got != "547" {
|
||||
t.Errorf("got %q, want %q", got, "547")
|
||||
}
|
||||
}
|
||||
|
||||
func TestULIConverter_EmptyPath(t *testing.T) {
|
||||
conv, err := NewULIConverter("*3gpp_uli")
|
||||
if err != nil {
|
||||
t.Fatalf("NewULIConverter failed: %v", err)
|
||||
}
|
||||
got, err := conv.Convert("8013006204d2")
|
||||
if err != nil {
|
||||
t.Fatalf("Convert failed: %v", err)
|
||||
}
|
||||
uli, ok := got.(*ULI)
|
||||
if !ok {
|
||||
t.Fatalf("expected *ULI, got %T", got)
|
||||
}
|
||||
if uli.TAI == nil {
|
||||
t.Fatal("TAI should not be nil")
|
||||
}
|
||||
if uli.TAI.MCC != "310" {
|
||||
t.Errorf("TAI.MCC: got %q, want %q", uli.TAI.MCC, "310")
|
||||
}
|
||||
}
|
||||
|
||||
func TestULIConverter_Errors(t *testing.T) {
|
||||
conv, err := NewULIConverter("*3gpp_uli:TAI.MCC")
|
||||
if err != nil {
|
||||
t.Fatalf("NewULIConverter failed: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input any
|
||||
}{
|
||||
{"empty string", ""},
|
||||
{"invalid hex", "zzzz"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := conv.Convert(tt.input)
|
||||
if err == nil {
|
||||
t.Error("Convert should have returned error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user