From 584a55ab2513ba8028a8d06efdf56da1a7dc11fc Mon Sep 17 00:00:00 2001 From: gezimbll Date: Wed, 10 Jan 2024 10:58:22 -0500 Subject: [PATCH] added *http filter type --- engine/filterhelpers.go | 53 ++++++++++++++++++++++++-- engine/filters.go | 29 ++++++++++++++- engine/filters_test.go | 82 +++++++++++++++++++++++++++++++++++++++++ utils/consts.go | 1 + 4 files changed, 160 insertions(+), 5 deletions(-) diff --git a/engine/filterhelpers.go b/engine/filterhelpers.go index 20bebdedd..42f08a8b4 100644 --- a/engine/filterhelpers.go +++ b/engine/filterhelpers.go @@ -197,7 +197,7 @@ func sentrypeerGetToken(tokenUrl, clientID, clientSecret, audience, grantType st utils.GrantTypeCfg: grantType, } jsonPayload, _ := json.Marshal(payload) - resp, err = getHTTP(http.MethodPost, tokenUrl, bytes.NewBuffer(jsonPayload), map[string][]string{utils.ContentType: {utils.JsonBody}}) + resp, err = getHTTP(http.MethodPost, tokenUrl, bytes.NewBuffer(jsonPayload), map[string]string{utils.ContentType: utils.JsonBody}) if err != nil { return } @@ -216,7 +216,7 @@ func sentrypeerGetToken(tokenUrl, clientID, clientSecret, audience, grantType st // sentrypeerHasData return a boolean based on query response on finding ip/number func sentrypeerHasData(itemId, token, url string) (found bool, err error) { var resp *http.Response - resp, err = getHTTP(http.MethodGet, url, nil, map[string][]string{utils.AuthorizationHdr: {fmt.Sprintf("%s %s", utils.BearerAuth, token)}}) + resp, err = getHTTP(http.MethodGet, url, nil, map[string]string{utils.AuthorizationHdr: fmt.Sprintf("%s %s", utils.BearerAuth, token)}) if err != nil { return } @@ -238,11 +238,56 @@ func sentrypeerHasData(itemId, token, url string) (found bool, err error) { return false, err } -func getHTTP(method, url string, payload io.Reader, headers map[string][]string) (resp *http.Response, err error) { +// send an http get request to a server +// 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) + if err != nil { + return false, err + } + if fieldname != utils.MetaAny { + queryParams := parsedURL.Query() + queryParams.Set(fieldname, value) + parsedURL.RawQuery = queryParams.Encode() + resp, err = getHTTP(http.MethodGet, 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) + } + + if err != nil { + return false, 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) + } + + if err := json.NewDecoder(resp.Body).Decode(&reply); err != nil { + return false, fmt.Errorf("error decoding response: %w", err) + } + + return +} + +// constructs an request via parameters provided,url,header and payload ,uses defaultclient for sending the request +func getHTTP(method, url string, payload io.Reader, headers map[string]string) (resp *http.Response, err error) { var req *http.Request if req, err = http.NewRequest(method, url, payload); err != nil { return } - req.Header = headers + for k, hVal := range headers { + req.Header.Add(k, hVal) + } return http.DefaultClient.Do(req) } diff --git a/engine/filters.go b/engine/filters.go index fe55dff11..689916d88 100644 --- a/engine/filters.go +++ b/engine/filters.go @@ -19,6 +19,7 @@ along with this program. If not, see package engine import ( + "errors" "fmt" "net" "reflect" @@ -226,7 +227,7 @@ func (fltr *Filter) Compile() (err error) { var supportedFiltersType utils.StringSet = utils.NewStringSet([]string{ utils.MetaString, utils.MetaContains, utils.MetaPrefix, utils.MetaSuffix, - utils.MetaTimings, utils.MetaRSR, utils.MetaDestinations, + utils.MetaTimings, utils.MetaRSR, utils.MetaDestinations, utils.MetaHTTP, utils.MetaEmpty, utils.MetaExists, utils.MetaLessThan, utils.MetaLessOrEqual, utils.MetaGreaterThan, utils.MetaGreaterOrEqual, utils.MetaEqual, utils.MetaIPNet, utils.MetaAPIBan, utils.MetaSentryPeer, utils.MetaActivationInterval, @@ -252,6 +253,9 @@ func NewFilterRule(rfType, fieldName string, vals []string) (*FilterRule, error) rType = utils.Meta + strings.TrimPrefix(rfType, utils.MetaNot) negative = true } + if strings.HasPrefix(rfType, utils.MetaHTTP) { + rType = utils.MetaHTTP + } if !supportedFiltersType.Has(rType) { return nil, fmt.Errorf("Unsupported filter Type: %s", rfType) } @@ -361,6 +365,10 @@ func (fltr *FilterRule) Pass(dDP utils.DataProvider) (result bool, err error) { case utils.MetaRegex, utils.MetaNotRegex: result, err = fltr.passRegex(dDP) default: + if strings.HasPrefix(fltr.Type, utils.MetaHTTP) && strings.Index(fltr.Type, "#") == len(utils.MetaHTTP) { + result, err = fltr.passHttp(dDP) + break + } err = utils.ErrPrefixNotErrNotImplemented(fltr.Type) } if err != nil { @@ -771,3 +779,22 @@ func (fltr *FilterRule) passRegex(dDP utils.DataProvider) (bool, error) { } return false, nil } + +func (fltr *FilterRule) passHttp(dDP utils.DataProvider) (bool, error) { + strVal, err := fltr.rsrElement.ParseDataProvider(dDP) + if err != nil { + if err == utils.ErrNotFound { + return false, nil + } + 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) + +} diff --git a/engine/filters_test.go b/engine/filters_test.go index e96245322..f54850017 100644 --- a/engine/filters_test.go +++ b/engine/filters_test.go @@ -2676,3 +2676,85 @@ func TestFilterPassTiming(t *testing.T) { } } + +func TestHttpInlineFilter(t *testing.T) { + + dP := &utils.MapStorage{ + utils.MetaReq: map[string]any{ + "Attribute": "AttributeProfile1", + utils.CGRID: "CGRATES_ID1", + utils.AccountField: "1002", + utils.AnswerTime: time.Date(2013, 12, 30, 14, 59, 31, 0, time.UTC), + }, + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + if len(r.URL.Query()) != 0 { + queryVal := r.URL.Query() + reply := queryVal.Has("~*req.Account") && queryVal.Get("~*req.Account") == "1002" + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, reply) + return + } + + var data map[string]any + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + _, has := data["*req"] + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, has) + + })) + url := "*http#" + "[" + srv.URL + "]" + + exp := &Filter{ + Tenant: "cgrates.org", + ID: url + ":" + "~*req.Account:", + Rules: []*FilterRule{ + { + Type: url, + Element: "~*req.Account", + }, + }, + } + if err := exp.Compile(); err != nil { + t.Fatal(err) + } + if fl, err := NewFilterFromInline("cgrates.org", url+":"+"~*req.Account:"); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, fl) { + t.Errorf("Expected: %s , received: %s", utils.ToJSON(exp), utils.ToJSON(fl)) + } + if pass, err := exp.Rules[0].Pass(dP); err != nil { + t.Error(err) + } else if !pass { + t.Error("should had passed") + } + + exp2 := &Filter{ + Tenant: "cgrates.org", + ID: url + ":" + "*any:", + Rules: []*FilterRule{ + { + Type: url, + Element: "*any", + }, + }, + } + + if fl2, err := NewFilterFromInline("cgrates.org", url+":"+"*any:"); err != nil { + t.Error(err) + } else if fl2.Rules[0].Type != exp2.Rules[0].Type { + t.Errorf("Expected: %s , received: %s", utils.ToJSON(exp2), utils.ToJSON(fl2)) + } else if pass, err := fl2.Rules[0].Pass(dP); err != nil { + t.Error(err) + } else if !pass { + t.Error("should had passed") + } +} diff --git a/utils/consts.go b/utils/consts.go index e69631421..55c215f2d 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1179,6 +1179,7 @@ const ( MetaActivationInterval = "*ai" MetaRegex = "*regex" MetaContains = "*contains" + MetaHTTP = "*http" MetaNotString = "*notstring" MetaNotPrefix = "*notprefix"