Files
cgrates/agents/fsagent.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

498 lines
19 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 agents
import (
"errors"
"fmt"
"strings"
"time"
"github.com/cgrates/birpc"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/sessions"
"github.com/cgrates/cgrates/utils"
"github.com/cgrates/fsock"
)
func NewFSsessions(fsAgentConfig *config.FsAgentCfg,
timezone string, connMgr *engine.ConnManager) (*FSsessions, error) {
fsa := &FSsessions{
cfg: fsAgentConfig,
conns: make([]*fsock.FSock, len(fsAgentConfig.EventSocketConns)),
senderPools: make([]*fsock.FSockPool, len(fsAgentConfig.EventSocketConns)),
timezone: timezone,
connMgr: connMgr,
}
srv, err := birpc.NewServiceWithMethodsRename(fsa, utils.SessionSv1, true, func(oldFn string) (newFn string) {
return strings.TrimPrefix(oldFn, "V1")
})
if err != nil {
return nil, err
}
fsa.ctx = context.WithClient(context.TODO(), srv)
return fsa, nil
}
// FSsessions is the freeswitch session manager
// it holds a buffer for the network connection
// and the active sessions
type FSsessions struct {
cfg *config.FsAgentCfg
conns []*fsock.FSock // Keep the list here for connection management purposes
senderPools []*fsock.FSockPool // Keep sender pools here
timezone string
connMgr *engine.ConnManager
ctx *context.Context
}
func (fsa *FSsessions) createHandlers() map[string][]func(string, int) {
ca := func(body string, connIdx int) {
fsa.onChannelAnswer(
NewFSEvent(body), connIdx)
}
ch := func(body string, connIdx int) {
fsa.onChannelHangupComplete(
NewFSEvent(body), connIdx)
}
handlers := map[string][]func(string, int){
"CHANNEL_ANSWER": {ca},
"CHANNEL_HANGUP_COMPLETE": {ch},
}
if fsa.cfg.SubscribePark {
cp := func(body string, connIdx int) {
fsa.onChannelPark(
NewFSEvent(body), connIdx)
}
handlers["CHANNEL_PARK"] = []func(string, int){cp}
}
return handlers
}
// Sets the call timeout valid of starting of the call
func (fsa *FSsessions) setMaxCallDuration(uuid string, connIdx int,
maxDur time.Duration, destNr string) error {
if len(fsa.cfg.EmptyBalanceContext) != 0 {
_, err := fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("uuid_setvar %s execute_on_answer sched_transfer +%d %s XML %s\n\n",
uuid, int(maxDur.Seconds()), destNr, fsa.cfg.EmptyBalanceContext))
if err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not transfer the call to empty balance context, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return err
}
return nil
}
if len(fsa.cfg.EmptyBalanceAnnFile) != 0 {
if _, err := fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("sched_broadcast +%d %s playback!manager_request::%s aleg\n\n",
int(maxDur.Seconds()), uuid, fsa.cfg.EmptyBalanceAnnFile)); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not send uuid_broadcast to freeswitch, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return err
}
return nil
}
_, err := fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("uuid_setvar %s execute_on_answer sched_hangup +%d alloted_timeout\n\n",
uuid, int(maxDur.Seconds())))
if err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not send sched_hangup command to freeswitch, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return err
}
return nil
}
// Sends the transfer command to unpark the call to freeswitch
func (fsa *FSsessions) unparkCall(uuid string, connIdx int, callDestNb, notify string) (err error) {
_, err = fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("uuid_setvar %s cgr_notify %s\n\n", uuid, notify))
if err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not send unpark api notification to freeswitch, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return
}
if _, err = fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("uuid_transfer %s %s\n\n", uuid, callDestNb)); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not send unpark api call to freeswitch, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
}
return
}
func (fsa *FSsessions) onChannelPark(fsev FSEvent, connIdx int) {
if fsev.GetReqType(utils.MetaDefault) == utils.MetaNone { // Not for us
return
}
if connIdx >= len(fsa.conns) { // protection against index out of range panic
err := fmt.Errorf("Index out of range[0,%v): %v ", len(fsa.conns), connIdx)
utils.Logger.Err(fmt.Sprintf("<%s> %s", utils.FreeSWITCHAgent, err.Error()))
return
}
fsev[VarCGROriginHost] = utils.FirstNonEmpty(fsev[VarCGROriginHost], fsa.cfg.EventSocketConns[connIdx].Alias) // rewrite the OriginHost variable if it is empty
authArgs := fsev.V1AuthorizeArgs()
authArgs.CGREvent.Event[FsConnID] = connIdx // Attach the connection ID
var authReply sessions.V1AuthorizeReply
if err := fsa.connMgr.Call(fsa.ctx, fsa.cfg.SessionSConns,
utils.SessionSv1AuthorizeEvent,
authArgs, &authReply); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not authorize event %s, error: %s",
utils.FreeSWITCHAgent, fsev.GetUUID(), err.Error()))
fsa.unparkCall(fsev.GetUUID(), connIdx,
fsev.GetCallDestNr(utils.MetaDefault), err.Error())
return
}
if authReply.Attributes != nil {
for _, fldName := range authReply.Attributes.AlteredFields {
fldName = strings.TrimPrefix(fldName, utils.MetaReq+utils.NestingSep)
if _, has := authReply.Attributes.CGREvent.Event[fldName]; !has {
continue //maybe removed
}
if _, err := fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("uuid_setvar %s %s %s\n\n", fsev.GetUUID(), fldName,
authReply.Attributes.CGREvent.Event[fldName])); err != nil {
utils.Logger.Info(
fmt.Sprintf("<%s> error %s setting channel variabile: %s",
utils.FreeSWITCHAgent, err.Error(), fldName))
fsa.unparkCall(fsev.GetUUID(), connIdx,
fsev.GetCallDestNr(utils.MetaDefault), err.Error())
return
}
}
}
if authArgs.GetMaxUsage {
if authReply.MaxUsage == nil || *authReply.MaxUsage == 0 {
fsa.unparkCall(fsev.GetUUID(), connIdx,
fsev.GetCallDestNr(utils.MetaDefault), utils.ErrInsufficientCredit.Error())
return
}
fsa.setMaxCallDuration(fsev.GetUUID(), connIdx,
*authReply.MaxUsage, fsev.GetCallDestNr(utils.MetaDefault))
}
if authReply.ResourceAllocation != nil {
if _, err := fsa.conns[connIdx].SendApiCmd(fmt.Sprintf("uuid_setvar %s %s %s\n\n",
fsev.GetUUID(), CGRResourceAllocation, *authReply.ResourceAllocation)); err != nil {
utils.Logger.Info(
fmt.Sprintf("<%s> error %s setting channel variabile: %s",
utils.FreeSWITCHAgent, err.Error(), CGRResourceAllocation))
fsa.unparkCall(fsev.GetUUID(), connIdx,
fsev.GetCallDestNr(utils.MetaDefault), err.Error())
return
}
}
if authReply.RouteProfiles != nil {
fsArray := SliceAsFsArray(authReply.RouteProfiles.RoutesWithParams())
if _, err := fsa.conns[connIdx].SendApiCmd(fmt.Sprintf("uuid_setvar %s %s %s\n\n",
fsev.GetUUID(), utils.CGRRoutes, fsArray)); err != nil {
utils.Logger.Info(fmt.Sprintf("<%s> error setting routes: %s",
utils.FreeSWITCHAgent, err.Error()))
fsa.unparkCall(fsev.GetUUID(), connIdx, fsev.GetCallDestNr(utils.MetaDefault), err.Error())
return
}
}
fsa.unparkCall(fsev.GetUUID(), connIdx,
fsev.GetCallDestNr(utils.MetaDefault), AUTH_OK)
}
func (fsa *FSsessions) onChannelAnswer(fsev FSEvent, connIdx int) {
if fsev.GetReqType(utils.MetaDefault) == utils.MetaNone { // Do not process this request
return
}
if connIdx >= len(fsa.conns) { // protection against index out of range panic
err := fmt.Errorf("Index out of range[0,%v): %v ", len(fsa.conns), connIdx)
utils.Logger.Err(fmt.Sprintf("<%s> %s", utils.FreeSWITCHAgent, err.Error()))
return
}
_, err := fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("uuid_setvar %s %s %s\n\n", fsev.GetUUID(),
utils.CGROriginHost, utils.FirstNonEmpty(fsa.cfg.EventSocketConns[connIdx].Alias,
fsa.cfg.EventSocketConns[connIdx].Address)))
if err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> error %s setting channel variabile: %s",
utils.FreeSWITCHAgent, err.Error(), VarCGROriginHost))
return
}
fsev[VarCGROriginHost] = utils.FirstNonEmpty(fsev[VarCGROriginHost], fsa.cfg.EventSocketConns[connIdx].Alias) // rewrite the OriginHost variable if it is empty
chanUUID := fsev.GetUUID()
if missing := fsev.MissingParameter(fsa.timezone); missing != "" {
fsa.disconnectSession(connIdx, chanUUID, "",
utils.NewErrMandatoryIeMissing(missing).Error())
return
}
initSessionArgs := fsev.V1InitSessionArgs()
initSessionArgs.CGREvent.Event[FsConnID] = connIdx // Attach the connection ID so we can properly disconnect later
var initReply sessions.V1InitSessionReply
if err := fsa.connMgr.Call(fsa.ctx, fsa.cfg.SessionSConns,
utils.SessionSv1InitiateSession,
initSessionArgs, &initReply); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> could not process answer for event %s, error: %s",
utils.FreeSWITCHAgent, chanUUID, err.Error()))
fsa.disconnectSession(connIdx, chanUUID, "", err.Error())
return
}
}
func (fsa *FSsessions) onChannelHangupComplete(fsev FSEvent, connIdx int) {
if fsev.GetReqType(utils.MetaDefault) == utils.MetaNone { // Do not process this request
return
}
if connIdx >= len(fsa.conns) { // protection against index out of range panic
err := fmt.Errorf("Index out of range[0,%v): %v ", len(fsa.conns), connIdx)
utils.Logger.Err(fmt.Sprintf("<%s> %s", utils.FreeSWITCHAgent, err.Error()))
return
}
var reply string
fsev[VarCGROriginHost] = utils.FirstNonEmpty(fsev[VarCGROriginHost], fsa.cfg.EventSocketConns[connIdx].Alias) // rewrite the OriginHost variable if it is empty
if fsev[VarAnswerEpoch] != "0" { // call was answered
terminateSessionArgs := fsev.V1TerminateSessionArgs()
terminateSessionArgs.CGREvent.Event[FsConnID] = connIdx // Attach the connection ID in case we need to create a session and disconnect it
if err := fsa.connMgr.Call(fsa.ctx, fsa.cfg.SessionSConns,
utils.SessionSv1TerminateSession,
terminateSessionArgs, &reply); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not terminate session with event %s, error: %s",
utils.FreeSWITCHAgent, fsev.GetUUID(), err.Error()))
}
}
if fsa.cfg.CreateCdr {
cgrEv, err := fsev.AsCGREvent(fsa.timezone)
if err != nil {
return
}
if err := fsa.connMgr.Call(fsa.ctx, fsa.cfg.SessionSConns,
utils.SessionSv1ProcessCDR,
cgrEv, &reply); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Failed processing CGREvent: %s, error: <%s>",
utils.FreeSWITCHAgent, utils.ToJSON(cgrEv), err.Error()))
}
}
}
// Connect connects to the freeswitch mod_event_socket server and starts
// listening for events.
func (fsa *FSsessions) Connect() error {
eventFilters := map[string][]string{"Call-Direction": {"inbound"}}
errChan := make(chan error)
for connIdx, connCfg := range fsa.cfg.EventSocketConns {
fSock, err := fsock.NewFSock(connCfg.Address, connCfg.Password, connCfg.Reconnects, connCfg.MaxReconnectInterval, utils.FibDuration,
fsa.createHandlers(), eventFilters, utils.Logger, connIdx, true)
if err != nil {
return err
}
if !fSock.Connected() {
return errors.New("Could not connect to FreeSWITCH")
}
fsa.conns[connIdx] = fSock
utils.Logger.Info(fmt.Sprintf("<%s> successfully connected to FreeSWITCH at: <%s>", utils.FreeSWITCHAgent, connCfg.Address))
go func(fsock *fsock.FSock) { // Start reading in own goroutine, return on error
if err := fsock.ReadEvents(); err != nil {
errChan <- err
}
}(fSock)
fsSenderPool := fsock.NewFSockPool(5, connCfg.Address, connCfg.Password, 1, fsa.cfg.MaxWaitConnection,
0, utils.FibDuration, make(map[string][]func(string, int)), make(map[string][]string), utils.Logger, connIdx, true)
if fsSenderPool == nil {
return errors.New("Cannot connect FreeSWITCH senders pool")
}
fsa.senderPools[connIdx] = fsSenderPool
}
err := <-errChan // Will keep the Connect locked until the first error in one of the connections
return err
}
// fsev.GetCallDestNr(utils.MetaDefault)
// Disconnects a session by sending hangup command to freeswitch
func (fsa *FSsessions) disconnectSession(connIdx int, uuid, redirectNr, notify string) error {
if _, err := fsa.conns[connIdx].SendApiCmd(
fmt.Sprintf("uuid_setvar %s cgr_notify %s\n\n", uuid, notify)); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> error: %s when attempting to disconnect channelID: %s over connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), uuid, connIdx))
return err
}
if notify == utils.ErrInsufficientCredit.Error() {
if len(fsa.cfg.EmptyBalanceContext) != 0 {
if _, err := fsa.conns[connIdx].SendApiCmd(fmt.Sprintf("uuid_transfer %s %s XML %s\n\n",
uuid, redirectNr, fsa.cfg.EmptyBalanceContext)); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Could not transfer the call to empty balance context, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return err
}
return nil
}
if len(fsa.cfg.EmptyBalanceAnnFile) != 0 {
if _, err := fsa.conns[connIdx].SendApiCmd(fmt.Sprintf("uuid_broadcast %s playback!manager_request::%s aleg\n\n",
uuid, fsa.cfg.EmptyBalanceAnnFile)); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Could not send uuid_broadcast to freeswitch, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return err
}
return nil
}
}
if err := fsa.conns[connIdx].SendMsgCmd(uuid,
map[string]string{"call-command": "hangup", "hangup-cause": "MANAGER_REQUEST"}); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> Could not send disconnect msg to freeswitch, error: <%s>, connIdx: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return err
}
return nil
}
// Shutdown stops all connected fsock connections
func (fsa *FSsessions) Shutdown() (err error) {
for connIdx, fSock := range fsa.conns {
if fSock == nil ||
!fSock.Connected() {
utils.Logger.Err(fmt.Sprintf("<%s> Cannot shutdown sessions, fsock not connected for connection index: %v", utils.FreeSWITCHAgent, connIdx))
continue
}
utils.Logger.Info(fmt.Sprintf("<%s> Shutting down all sessions on connection index: %v", utils.FreeSWITCHAgent, connIdx))
if _, err = fSock.SendApiCmd("hupall MANAGER_REQUEST cgr_reqtype *prepaid"); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Error on calls shutdown: %s, connection index: %v", utils.FreeSWITCHAgent, err.Error(), connIdx))
}
if err = fSock.Disconnect(); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Error on disconnect: %s, connection index: %v", utils.FreeSWITCHAgent, err.Error(), connIdx))
}
}
return
}
// Call implements birpc.ClientConnector interface
func (fsa *FSsessions) Call(ctx *context.Context, serviceMethod string, args any, reply any) error {
return utils.RPCCall(fsa, serviceMethod, args, reply)
}
// V1DisconnectSession internal method to disconnect session in FreeSWITCH
func (fsa *FSsessions) V1DisconnectSession(ctx *context.Context, args utils.AttrDisconnectSession, reply *string) (err error) {
ev := engine.NewMapEvent(args.EventStart)
channelID := ev.GetStringIgnoreErrors(utils.OriginID)
connIdx, err := ev.GetTInt64(FsConnID)
if err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> error: <%s:%s> when attempting to disconnect channelID: <%s>",
utils.FreeSWITCHAgent, err.Error(), FsConnID, channelID))
return
}
if int(connIdx) >= len(fsa.conns) { // protection against index out of range panic
err := fmt.Errorf("Index out of range[0,%v): %v ", len(fsa.conns), connIdx)
utils.Logger.Err(fmt.Sprintf("<%s> %s", utils.FreeSWITCHAgent, err.Error()))
return err
}
if err = fsa.disconnectSession(int(connIdx), channelID,
utils.FirstNonEmpty(ev.GetStringIgnoreErrors(CALL_DEST_NR), ev.GetStringIgnoreErrors(SIP_REQ_USER)),
args.Reason); err != nil {
return
}
*reply = utils.OK
return
}
// V1GetActiveSessionIDs used to return all active sessions
func (fsa *FSsessions) V1GetActiveSessionIDs(ctx *context.Context, _ string,
sessionIDs *[]*sessions.SessionID) (err error) {
var sIDs []*sessions.SessionID
for connIdx, senderPool := range fsa.senderPools {
fsConn, err := senderPool.PopFSock()
if err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Error on pop FSock: %s, connection index: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
continue
}
activeChanStr, err := fsConn.SendApiCmd("show channels")
senderPool.PushFSock(fsConn)
if err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Error on push FSock: %s, connection index: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
continue
}
for _, fsAChan := range fsock.MapChanData(activeChanStr) {
sIDs = append(sIDs, &sessions.SessionID{
OriginHost: fsa.cfg.EventSocketConns[connIdx].Alias,
OriginID: fsAChan["uuid"],
})
}
}
if len(sIDs) == 0 {
return utils.ErrNoActiveSession
}
*sessionIDs = sIDs
return
}
// Reload recreates the connection buffers
// only used on reload
func (fsa *FSsessions) Reload() {
fsa.conns = make([]*fsock.FSock, len(fsa.cfg.EventSocketConns))
fsa.senderPools = make([]*fsock.FSockPool, len(fsa.cfg.EventSocketConns))
}
// V1WarnDisconnect is called when call goes under the minimum duration threshold, so FreeSWITCH can play an announcement message
func (fsa *FSsessions) V1WarnDisconnect(ctx *context.Context, args map[string]any, reply *string) (err error) {
if fsa.cfg.LowBalanceAnnFile == utils.EmptyString {
*reply = utils.OK
return
}
ev := engine.NewMapEvent(args)
channelID := ev.GetStringIgnoreErrors(utils.OriginID)
var connIdx int64
if connIdx, err = ev.GetTInt64(FsConnID); err != nil {
utils.Logger.Err(
fmt.Sprintf("<%s> error: <%s:%s> when attempting to disconnect channelID: <%s>",
utils.FreeSWITCHAgent, err.Error(), FsConnID, channelID))
return
}
if int(connIdx) >= len(fsa.conns) { // protection against index out of range panic
err = fmt.Errorf("Index out of range[0,%v): %v ", len(fsa.conns), connIdx)
utils.Logger.Err(fmt.Sprintf("<%s> %s", utils.FreeSWITCHAgent, err.Error()))
return
}
if _, err = fsa.conns[connIdx].SendApiCmd(fmt.Sprintf("uuid_broadcast %s playback!manager_request::%s aleg\n\n", channelID, fsa.cfg.LowBalanceAnnFile)); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> Could not send uuid_broadcast to freeswitch, error: %s, connection id: %v",
utils.FreeSWITCHAgent, err.Error(), connIdx))
return
}
*reply = utils.OK
return
}
// V1ReAuthorize is used to implement the sessions.BiRPClient interface
func (*FSsessions) V1ReAuthorize(*context.Context, utils.CGREvent, *string) error {
return utils.ErrNotImplemented
}
// V1DisconnectPeer is used to implement the sessions.BiRPClient interface
func (*FSsessions) V1DisconnectPeer(*context.Context, *utils.DPRArgs, *string) error {
return utils.ErrNotImplemented
}