mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
added *http attribute type
This commit is contained in:
committed by
Dan Christian Bogos
parent
584a55ab25
commit
6f6374abb9
@@ -560,6 +560,10 @@ func ParseAttribute(dp utils.DataProvider, attrType, path string, value config.R
|
||||
sort.Strings(values[1:])
|
||||
out = strings.Join(values, utils.InfieldSep)
|
||||
default:
|
||||
if strings.HasPrefix(attrType, utils.MetaHTTP) {
|
||||
out, err = externalAttributeAPI(attrType, dp)
|
||||
break
|
||||
}
|
||||
return utils.EmptyString, fmt.Errorf("unsupported type: <%s>", attrType)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -1580,3 +1580,50 @@ func TestAttributesV1ProcessEventSentryPeer(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAttributeFromHTTP(t *testing.T) {
|
||||
exp := "Account"
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, exp)
|
||||
|
||||
}))
|
||||
|
||||
defer testServer.Close()
|
||||
attrType := utils.MetaHTTP + utils.HashtagSep + utils.IdxStart + testServer.URL + utils.IdxEnd
|
||||
|
||||
attrID := attrType + ":*req.Category:*attributes"
|
||||
expAttrPrf1 := &AttributeProfile{
|
||||
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
|
||||
ID: attrType + ":*req.Category:*attributes",
|
||||
Contexts: []string{utils.MetaAny},
|
||||
Attributes: []*Attribute{
|
||||
{
|
||||
Path: utils.MetaReq + utils.NestingSep + "Category",
|
||||
Type: attrType,
|
||||
Value: config.NewRSRParsersMustCompile("*attributes", utils.InfieldSep),
|
||||
},
|
||||
},
|
||||
}
|
||||
attrPrf, err := NewAttributeFromInline(config.CgrConfig().GeneralCfg().DefaultTenant, attrID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(expAttrPrf1, attrPrf) {
|
||||
t.Errorf("Expecting %+v, received: %+v", utils.ToJSON(expAttrPrf1), utils.ToJSON(attrPrf))
|
||||
}
|
||||
dp := utils.MapStorage{
|
||||
utils.MetaReq: utils.MapStorage{},
|
||||
}
|
||||
|
||||
attr := attrPrf.Attributes[0]
|
||||
if out, err := ParseAttribute(dp, attr.Type, attr.Path, attr.Value,
|
||||
0, utils.EmptyString, utils.EmptyString, utils.InfieldSep); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exp != out {
|
||||
t.Errorf("Expected %q, Received %q", exp, out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@ package engine
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/guardian"
|
||||
@@ -242,9 +244,18 @@ func sentrypeerHasData(itemId, token, url string) (found bool, err error) {
|
||||
// expects a boolean reply
|
||||
// when element is set to *any the CGREvent is sent as JSON body
|
||||
// when the element is specified as a path e.g ~*req.Account is sent as query string pair ,the path being the key with the value extracted from dataprovider
|
||||
func externalAPI(urlStr string, dDP any, fieldname, value string) (reply bool, err error) {
|
||||
var resp *http.Response
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
func filterHTTP(httpType string, dDP any, fieldname, value string) (bool, error) {
|
||||
var (
|
||||
parsedURL *url.URL
|
||||
resp string
|
||||
err error
|
||||
)
|
||||
urlS, err := extractUrlFromType(httpType)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
parsedURL, err = url.Parse(urlS)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -252,32 +263,40 @@ func externalAPI(urlStr string, dDP any, fieldname, value string) (reply bool, e
|
||||
queryParams := parsedURL.Query()
|
||||
queryParams.Set(fieldname, value)
|
||||
parsedURL.RawQuery = queryParams.Encode()
|
||||
resp, err = getHTTP(http.MethodGet, parsedURL.String(), nil, nil)
|
||||
resp, err = externalAPI(parsedURL.String(), nil, nil)
|
||||
} else {
|
||||
var data []byte
|
||||
data, err = json.Marshal(dDP)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error marshaling data: %w", err)
|
||||
}
|
||||
resp, err = getHTTP(http.MethodGet, parsedURL.String(), bytes.NewReader(data), nil)
|
||||
resp, err = externalAPI(parsedURL.String(), bytes.NewReader(data), nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error processing the request: %w", err)
|
||||
return false, err
|
||||
}
|
||||
return utils.IfaceAsBool(resp)
|
||||
}
|
||||
|
||||
func externalAPI(url string, rdr io.Reader, hdr map[string]string) (string, error) {
|
||||
resp, err := getHTTP(http.MethodGet, url, rdr, hdr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error processing the request: %w", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusMultipleChoices {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
return false, fmt.Errorf("http request returned non-OK status code: %d ,body: %v ,err: %w", resp.StatusCode, string(body), err)
|
||||
return "", fmt.Errorf("http request returned non-OK status code: %d ,body: %v ,err: %w", resp.StatusCode, string(body), err)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&reply); err != nil {
|
||||
return false, fmt.Errorf("error decoding response: %w", err)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
return
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// constructs an request via parameters provided,url,header and payload ,uses defaultclient for sending the request
|
||||
@@ -291,3 +310,13 @@ func getHTTP(method, url string, payload io.Reader, headers map[string]string) (
|
||||
}
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
func extractUrlFromType(httpType string) (string, error) {
|
||||
parts := strings.Split(httpType, utils.HashtagSep)
|
||||
if len(parts) != 2 {
|
||||
return "", errors.New("url is not specified")
|
||||
}
|
||||
//extracting the url from the type
|
||||
url := strings.Trim(parts[1], utils.IdxStart+utils.IdxEnd)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
@@ -789,12 +788,6 @@ func (fltr *FilterRule) passHttp(dDP utils.DataProvider) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
parts := strings.Split(fltr.Type, utils.HashtagSep)
|
||||
if len(parts) != 2 {
|
||||
return false, errors.New("url is not specified")
|
||||
}
|
||||
//extracting the url from the type
|
||||
url := strings.Trim(parts[1], "[]")
|
||||
return externalAPI(url, dDP, fltr.Element, strVal)
|
||||
return filterHTTP(fltr.Type, dDP, fltr.Element, strVal)
|
||||
|
||||
}
|
||||
|
||||
@@ -2711,6 +2711,7 @@ func TestHttpInlineFilter(t *testing.T) {
|
||||
fmt.Fprint(w, has)
|
||||
|
||||
}))
|
||||
defer srv.Close()
|
||||
url := "*http#" + "[" + srv.URL + "]"
|
||||
|
||||
exp := &Filter{
|
||||
|
||||
@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -148,8 +150,8 @@ func NewAttributeFromInline(tenant, inlnRule string) (attr *AttributeProfile, er
|
||||
Contexts: []string{utils.MetaAny},
|
||||
}
|
||||
for _, rule := range strings.Split(inlnRule, utils.InfieldSep) {
|
||||
ruleSplt := strings.SplitN(rule, utils.InInFieldSep, 3)
|
||||
if len(ruleSplt) < 3 {
|
||||
ruleSplt := utils.SplitPath(rule, utils.InInFieldSep[0], 3)
|
||||
if len(ruleSplt) != 3 {
|
||||
return nil, fmt.Errorf("inline parse error for string: <%s>", rule)
|
||||
}
|
||||
var vals config.RSRParsers
|
||||
@@ -168,3 +170,15 @@ func NewAttributeFromInline(tenant, inlnRule string) (attr *AttributeProfile, er
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func externalAttributeAPI(httpType string, dDP utils.DataProvider) (string, error) {
|
||||
urlS, err := extractUrlFromType(httpType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := json.Marshal(dDP)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshaling data: %w", err)
|
||||
}
|
||||
return externalAPI(urlS, bytes.NewReader(data), nil)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -2633,6 +2637,104 @@ func TestProcessAttributeSuffix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessAttributeHTTP(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
responses := map[string]struct {
|
||||
code int
|
||||
reply string
|
||||
}{
|
||||
"/passwd": {code: http.StatusOK, reply: "passwd1"},
|
||||
"/account": {code: http.StatusOK, reply: "1001"},
|
||||
}
|
||||
if val, has := responses[r.URL.EscapedPath()]; has {
|
||||
w.WriteHeader(val.code)
|
||||
fmt.Fprint(w, val.reply)
|
||||
return
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
}))
|
||||
defer ts.Close()
|
||||
cfg := config.NewDefaultCGRConfig()
|
||||
cfg.AttributeSCfg().Opts.ProcessRuns = 1
|
||||
data := NewInternalDB(nil, nil, true, cfg.DataDbCfg().Items)
|
||||
dmAtr = NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
|
||||
attrS = NewAttributeService(dmAtr, &FilterS{dm: dmAtr, cfg: cfg}, cfg)
|
||||
|
||||
//refresh the DM
|
||||
if err := dmAtr.DataDB().Flush(""); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
Cache.Clear(nil)
|
||||
|
||||
attrPrf := &AttributeProfile{
|
||||
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
|
||||
ID: "ATTR_HTTP",
|
||||
Contexts: []string{utils.MetaSessionS},
|
||||
FilterIDs: []string{"*string:~*req.Field1:Value1"},
|
||||
Attributes: []*Attribute{
|
||||
{
|
||||
Path: utils.MetaReq + utils.NestingSep + utils.AccountField,
|
||||
Type: utils.MetaHTTP + utils.HashtagSep + utils.IdxStart + ts.URL + "/account" + utils.IdxEnd,
|
||||
Value: config.NewRSRParsersMustCompile("*attributes", utils.InfieldSep),
|
||||
},
|
||||
{
|
||||
Path: utils.MetaReq + utils.NestingSep + "Password",
|
||||
Type: utils.MetaHTTP + utils.HashtagSep + utils.IdxStart + ts.URL + "/passwd" + utils.IdxEnd,
|
||||
Value: config.NewRSRParsersMustCompile("*attributes", utils.InfieldSep),
|
||||
},
|
||||
},
|
||||
Weight: 10,
|
||||
}
|
||||
// Add attribute in DM
|
||||
if err := dmAtr.SetAttributeProfile(attrPrf, true); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
attrArgs := &utils.CGREvent{
|
||||
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
|
||||
ID: utils.GenUUID(),
|
||||
Event: map[string]any{
|
||||
"Field1": "Value1",
|
||||
},
|
||||
APIOpts: map[string]any{
|
||||
utils.OptsAttributesProcessRuns: 1,
|
||||
utils.OptsContext: utils.MetaSessionS,
|
||||
},
|
||||
}
|
||||
eRply := &AttrSProcessEventReply{
|
||||
MatchedProfiles: []string{"cgrates.org:ATTR_HTTP"},
|
||||
AlteredFields: []string{utils.MetaReq + utils.NestingSep + utils.AccountField, utils.MetaReq + utils.NestingSep + "Password"},
|
||||
CGREvent: &utils.CGREvent{
|
||||
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
|
||||
ID: "TestProcessAttributeHTTP",
|
||||
Event: map[string]any{
|
||||
"Field1": "Value1",
|
||||
"Account": "1001",
|
||||
"Password": "passwd1",
|
||||
},
|
||||
},
|
||||
}
|
||||
var reply AttrSProcessEventReply
|
||||
if err := attrS.V1ProcessEvent(context.Background(), attrArgs, &reply); err != nil {
|
||||
t.Errorf("Error: %+v", err)
|
||||
}
|
||||
if !slices.Equal(eRply.MatchedProfiles, reply.MatchedProfiles) {
|
||||
t.Errorf("Expecting %+v, received: %+v", eRply.MatchedProfiles, reply.MatchedProfiles)
|
||||
}
|
||||
if sort.Slice(reply.AlteredFields, func(i, j int) bool {
|
||||
return reply.AlteredFields[i] < reply.AlteredFields[j]
|
||||
}); !slices.Equal(reply.AlteredFields, eRply.AlteredFields) {
|
||||
t.Errorf("Expecting %+v, received: %+v", eRply.AlteredFields, reply.AlteredFields)
|
||||
}
|
||||
if !reflect.DeepEqual(eRply.CGREvent.Event, reply.CGREvent.Event) {
|
||||
t.Errorf("Expecting %+v, received: %+v", eRply.CGREvent.Event, reply.CGREvent.Event)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAttributeIndexSelectsFalse(t *testing.T) {
|
||||
// change the IndexedSelects to false
|
||||
cfg := config.NewDefaultCGRConfig()
|
||||
|
||||
Reference in New Issue
Block a user