Files
cgrates/sessions/libsessions.go
ionutboangiu d8ad760dbb Update both ReAuthorize API signatures
The one from sessions takes an additional event alongside the
SessionFilter, while the one from agents will accept a CGREvent
instead of a simple originID string

The additional event sent to SessionSv1ReAuthorize will be merged
with the EventStart event from the matched session and can be used
when building server initiated requests from the *req map. The
initial packet which was initially inside *req, will be moved to
the *oreq ExtraDP (stands for original request).
2024-02-14 10:15:51 +01:00

296 lines
9.6 KiB
Go

/*
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 <http://www.gnu.org/licenses/>
*/
package sessions
import (
"errors"
"slices"
"strings"
"time"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
jwt "github.com/dgrijalva/jwt-go"
)
var unratedReqs = engine.MapEvent{
utils.MetaPostpaid: struct{}{},
utils.MetaPseudoPrepaid: struct{}{},
utils.MetaRated: struct{}{},
}
var authReqs = engine.MapEvent{
utils.MetaPrepaid: struct{}{},
utils.MetaPseudoPrepaid: struct{}{},
}
// BiRPCClient is the interface implemented by Agents which are able to
// communicate bidirectionally with SessionS and remote Communication Switch
type BiRPCClient interface {
V1DisconnectSession(*context.Context, utils.AttrDisconnectSession, *string) error
V1GetActiveSessionIDs(*context.Context, string, *[]*SessionID) error
V1ReAuthorize(*context.Context, utils.CGREvent, *string) error
V1DisconnectPeer(*context.Context, *utils.DPRArgs, *string) error
V1WarnDisconnect(*context.Context, map[string]any, *string) error
}
// GetSetCGRID will populate the CGRID key if not present and return it
func GetSetCGRID(ev engine.MapEvent) (cgrID string) {
cgrID = ev.GetStringIgnoreErrors(utils.CGRID)
if cgrID == "" {
cgrID = utils.Sha1(ev.GetStringIgnoreErrors(utils.OriginID),
ev.GetStringIgnoreErrors(utils.OriginHost))
ev[utils.CGRID] = cgrID
}
return
}
func getFlagIDs(flag string) []string {
flagWithIDs := strings.Split(flag, utils.InInFieldSep)
if len(flagWithIDs) <= 1 {
return nil
}
return strings.Split(flagWithIDs[1], utils.ANDSep)
}
// ProcessedStirIdentity the structure that keeps all the header information
type ProcessedStirIdentity struct {
Tokens []string
SigningStr string
Signature string
Header *utils.PASSporTHeader
Payload *utils.PASSporTPayload
}
// NewProcessedIdentity creates a proccessed header
func NewProcessedIdentity(identity string) (pi *ProcessedStirIdentity, err error) {
pi = new(ProcessedStirIdentity)
hdrtoken := strings.Split(utils.RemoveWhiteSpaces(identity), utils.InfieldSep)
if len(hdrtoken) == 1 {
err = errors.New("missing parts of the message header")
return
}
pi.Tokens = hdrtoken[1:]
btoken := strings.Split(hdrtoken[0], utils.NestingSep)
if len(btoken) != 3 {
err = errors.New("wrong header format")
return
}
pi.SigningStr = btoken[0] + utils.NestingSep + btoken[1]
pi.Signature = btoken[2]
pi.Header = new(utils.PASSporTHeader)
if err = utils.DecodeBase64JSON(btoken[0], pi.Header); err != nil {
return
}
pi.Payload = new(utils.PASSporTPayload)
err = utils.DecodeBase64JSON(btoken[1], pi.Payload)
return
}
// VerifyHeader returns if the header is corectly populated
func (pi *ProcessedStirIdentity) VerifyHeader() (isValid bool) {
var x5u string
for _, pair := range pi.Tokens {
ptoken := strings.Split(pair, utils.AttrValueSep)
if len(ptoken) != 2 {
continue
}
switch ptoken[0] {
case utils.STIRAlgField:
if ptoken[1] != utils.STIRAlg {
return false
}
case utils.STIRPptField:
if ptoken[1] != utils.STIRPpt &&
ptoken[1] != "\""+utils.STIRPpt+"\"" {
return false
}
case utils.STIRInfoField:
lenParamInfo := len(ptoken[1])
if lenParamInfo <= 2 {
return false
}
x5u = ptoken[1]
if x5u[0] == '<' && x5u[lenParamInfo-1] == '>' {
x5u = x5u[1 : lenParamInfo-1]
}
}
}
return pi.Header.Alg == utils.STIRAlg &&
pi.Header.Ppt == utils.STIRPpt &&
pi.Header.Typ == utils.STIRTyp &&
pi.Header.X5u == x5u
}
// VerifySignature returns if the signature is valid
func (pi *ProcessedStirIdentity) VerifySignature(timeoutVal time.Duration) (err error) {
var pubkey any
var ok bool
if pubkey, ok = engine.Cache.Get(utils.CacheSTIR, pi.Header.X5u); !ok {
if pubkey, err = utils.NewECDSAPubKey(pi.Header.X5u, timeoutVal); err != nil {
if errCh := engine.Cache.Set(utils.CacheSTIR, pi.Header.X5u, nil,
nil, false, utils.NonTransactional); errCh != nil {
return errCh
}
return
}
if errCh := engine.Cache.Set(utils.CacheSTIR, pi.Header.X5u, pubkey,
nil, false, utils.NonTransactional); errCh != nil {
return errCh
}
}
sigMethod := jwt.GetSigningMethod(pi.Header.Alg)
return sigMethod.Verify(pi.SigningStr, pi.Signature, pubkey)
}
// VerifyPayload returns if the payload is corectly populated
func (pi *ProcessedStirIdentity) VerifyPayload(originatorTn, originatorURI, destinationTn, destinationURI string,
hdrMaxDur time.Duration, attest utils.StringSet) (err error) {
if !attest.Has(utils.MetaAny) && !attest.Has(pi.Payload.ATTest) {
return errors.New("wrong attest level")
}
if hdrMaxDur >= 0 && time.Now().After(time.Unix(pi.Payload.IAT, 0).Add(hdrMaxDur)) {
return errors.New("expired payload")
}
if originatorURI != utils.EmptyString {
if originatorURI != pi.Payload.Orig.URI {
return errors.New("wrong originatorURI")
}
} else if originatorTn != pi.Payload.Orig.Tn {
return errors.New("wrong originatorTn")
}
if destinationURI != utils.EmptyString {
if !slices.Contains(pi.Payload.Dest.URI, destinationURI) {
return errors.New("wrong destinationURI")
}
} else if !slices.Contains(pi.Payload.Dest.Tn, destinationTn) {
return errors.New("wrong destinationTn")
}
return
}
// NewSTIRIdentity returns the identiy for stir header
func NewSTIRIdentity(header *utils.PASSporTHeader, payload *utils.PASSporTPayload, prvkeyPath string, timeout time.Duration) (identity string, err error) {
var prvKey any
var ok bool
if prvKey, ok = engine.Cache.Get(utils.CacheSTIR, prvkeyPath); !ok {
if prvKey, err = utils.NewECDSAPrvKey(prvkeyPath, timeout); err != nil {
if errCh := engine.Cache.Set(utils.CacheSTIR, prvkeyPath, nil,
nil, false, utils.NonTransactional); errCh != nil {
return utils.EmptyString, errCh
}
return
}
if errCh := engine.Cache.Set(utils.CacheSTIR, prvkeyPath, prvKey,
nil, false, utils.NonTransactional); errCh != nil {
return utils.EmptyString, errCh
}
}
var headerStr, payloadStr string
if headerStr, err = utils.EncodeBase64JSON(header); err != nil {
return
}
if payloadStr, err = utils.EncodeBase64JSON(payload); err != nil {
return
}
identity = headerStr + utils.NestingSep + payloadStr
sigMethod := jwt.GetSigningMethod(header.Alg)
var signature string
if signature, err = sigMethod.Sign(identity, prvKey); err != nil {
return
}
identity += utils.NestingSep + signature
identity += utils.STIRExtraInfoPrefix + header.X5u + utils.STIRExtraInfoSuffix
return
}
// AuthStirShaken autentificates the given identity using STIR/SHAKEN
func AuthStirShaken(identity, originatorTn, originatorURI, destinationTn, destinationURI string,
attest utils.StringSet, hdrMaxDur time.Duration) (err error) {
var pi *ProcessedStirIdentity
if pi, err = NewProcessedIdentity(identity); err != nil {
return
}
if !pi.VerifyHeader() {
return errors.New("wrong header")
}
if err = pi.VerifySignature(config.CgrConfig().GeneralCfg().ReplyTimeout); err != nil {
return
}
return pi.VerifyPayload(originatorTn, originatorURI, destinationTn, destinationURI, hdrMaxDur, attest)
}
// V1STIRAuthenticateArgs are the arguments for STIRAuthenticate API
type V1STIRAuthenticateArgs struct {
Attest []string // what attest levels are allowed
DestinationTn string // the expected destination telephone number
DestinationURI string // the expected destination URI; if this is populated the DestinationTn is ignored
Identity string // the identity header
OriginatorTn string // the expected originator telephone number
OriginatorURI string // the expected originator URI; if this is populated the OriginatorTn is ignored
PayloadMaxDuration string // the duration the payload is valid after it's creation
APIOpts map[string]any
}
// V1STIRIdentityArgs are the arguments for STIRIdentity API
type V1STIRIdentityArgs struct {
Payload *utils.PASSporTPayload // the STIR payload
PublicKeyPath string // the path to the public key used in the header
PrivateKeyPath string // the private key path
OverwriteIAT bool // if true the IAT from payload is overwrited with the present unix timestamp
APIOpts map[string]any
}
// getDerivedEvents returns only the *raw event if derivedReply flag is not specified
func getDerivedEvents(events map[string]*utils.CGREvent, derivedReply bool) map[string]*utils.CGREvent {
if derivedReply {
return events
}
return map[string]*utils.CGREvent{
utils.MetaRaw: events[utils.MetaRaw],
}
}
// getDerivedMaxUsage returns only the *raw MaxUsage if derivedReply flag is not specified
func getDerivedMaxUsage(maxUsages map[string]time.Duration, derivedReply bool) (out map[string]time.Duration) {
if derivedReply {
out = maxUsages
} else {
out = make(map[string]time.Duration)
}
var maxUsage time.Duration
var maxUsageSet bool // so we know if we have set the 0 on purpose
for _, rplyMaxUsage := range maxUsages {
if !maxUsageSet || rplyMaxUsage < maxUsage {
maxUsage = rplyMaxUsage
maxUsageSet = true
}
}
out[utils.MetaRaw] = maxUsage
return
}