/* 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 */ package agents import ( "bytes" "github.com/cgrates/cgrates/utils" "github.com/cgrates/radigo" ) // radAppendAttributes appends attributes to a RADIUS packet based on predefined template func radAppendAttributes(packet *radigo.Packet, nm *utils.OrderedNavigableMap) error { for el := nm.GetFirstElement(); el != nil; el = el.Next() { path := el.Value cfgItm, _ := nm.Field(path) path = path[:len(path)-1] // remove the last index if path[0] == MetaRadReplyCode { // Special case used to control the reply code of RADIUS reply if err := packet.SetCodeWithName(utils.IfaceAsString(cfgItm.Data)); err != nil { return err } continue } var attrName, vendorName string if len(path) > 1 { vendorName, attrName = path[0], path[1] } else { attrName = path[0] } if err := packet.AddAVPWithName(attrName, utils.IfaceAsString(cfgItm.Data), vendorName); err != nil { return err } } return nil } // newRADataProvider constructs a DataProvider func newRADataProvider(req *radigo.Packet) (dP utils.DataProvider) { dP = &radiusDP{req: req, cache: utils.MapStorage{}} return } // radiusDP implements utils.DataProvider, serving as radigo.Packet data decoder // decoded data is only searched once and cached type radiusDP struct { req *radigo.Packet cache utils.MapStorage } // String is part of utils.DataProvider interface // when called, it will display the already parsed values out of cache func (pk *radiusDP) String() string { return utils.ToIJSON(pk.req) // return ToJSON because Packet don't have a string method } // FieldAsInterface is part of utils.DataProvider interface func (pk *radiusDP) FieldAsInterface(fldPath []string) (data any, err error) { if len(fldPath) != 1 { return nil, utils.ErrNotFound } if data, err = pk.cache.FieldAsInterface(fldPath); err != nil { if err != utils.ErrNotFound { // item found in cache return } err = nil // cancel previous err } else { return // data found in cache } if len(pk.req.AttributesWithName(fldPath[0], "")) != 0 { data = pk.req.AttributesWithName(fldPath[0], "")[0].GetStringValue() } pk.cache.Set(fldPath, data) return } // FieldAsString is part of utils.DataProvider interface func (pk *radiusDP) FieldAsString(fldPath []string) (data string, err error) { var valIface any valIface, err = pk.FieldAsInterface(fldPath) if err != nil { return } return utils.IfaceAsString(valIface), nil } // radauthReq is used to authorize a request based on flags func radauthReq(flags utils.FlagsWithParams, req *radigo.Packet, aReq *AgentRequest, rpl *radigo.Packet) (bool, error) { // try to get UserPassword from Vars as slice of NMItems nmItems, has := aReq.Vars.Map[utils.UserPassword] if !has { return false, utils.ErrNotFound } pass := nmItems.Slice[0].Value.String() switch { case flags.Has(utils.MetaPAP): userPassAvps := req.AttributesWithName(UserPasswordAVP, utils.EmptyString) if len(userPassAvps) == 0 { return false, utils.NewErrMandatoryIeMissing(UserPasswordAVP) } return userPassAvps[0].StringValue == pass, nil case flags.Has(utils.MetaCHAP): chapAVPs := req.AttributesWithName(CHAPPasswordAVP, utils.EmptyString) if len(chapAVPs) == 0 { return false, utils.NewErrMandatoryIeMissing(CHAPPasswordAVP) } // RFC 2865: Use CHAP-Challenge AVP if present, otherwise Request Authenticator. challenge := req.Authenticator[:] if chapChallengeAVPs := req.AttributesWithName(CHAPChallengeAVP, utils.EmptyString); len(chapChallengeAVPs) > 0 { challenge = chapChallengeAVPs[0].RawValue } return radigo.AuthenticateCHAP([]byte(pass), challenge, chapAVPs[0].RawValue), nil case flags.Has(utils.MetaMSCHAPV2): msChallenge := req.AttributesWithName(MSCHAPChallengeAVP, MicrosoftVendor) if len(msChallenge) == 0 { return false, utils.NewErrMandatoryIeMissing(MSCHAPChallengeAVP) } msResponse := req.AttributesWithName(MSCHAP2ResponseAVP, MicrosoftVendor) if len(msResponse) == 0 { return false, utils.NewErrMandatoryIeMissing(MSCHAP2ResponseAVP) } vsaMSResponde := msResponse[0].Value.(*radigo.VSA).RawValue vsaMSChallange := msChallenge[0].Value.(*radigo.VSA).RawValue userName := req.AttributesWithName("User-Name", utils.EmptyString)[0].StringValue if len(vsaMSChallange) != 16 || len(vsaMSResponde) != 50 { return false, nil } ident := vsaMSResponde[0] peerChallenge := vsaMSResponde[2:18] peerResponse := vsaMSResponde[26:50] ntResponse, err := radigo.GenerateNTResponse(vsaMSChallange, peerChallenge, userName, pass) if err != nil || !bytes.Equal(ntResponse, peerResponse) { return false, err } authenticatorResponse, err := radigo.GenerateAuthenticatorResponse(vsaMSChallange, peerChallenge, ntResponse, userName, pass) if err != nil { return false, err } success := make([]byte, 43) success[0] = ident copy(success[1:], authenticatorResponse) // this AVP need to be added to be verified on the client side rpl.AddAVPWithName(MSCHAP2SuccessAVP, string(success), MicrosoftVendor) return true, nil default: return false, utils.NewErrMandatoryIeMissing(utils.Flags) } }