mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
`ClientConnector` is no longer defined within `rpcclient` in its latest
version. It has been changed to be obtained from the `cgrates/birpc`
library instead.
Replaced `net/rpc` with `cgrates/birpc` and `net/rpc/jsonrpc` with
`cgrates/birpc/jsonrpc` libraries.
The implementations of `CallBiRPC()` and `Handlers()` were removed,
along with the methods associated with them.
The `rpcclient.BIRPCConector` and the methods prefixed with `BiRPC` were
removed from the `BiRPClient` interface.
The `BiRPClient` interface was renamed to `BIRPCClient`, although not
sure if needed (seems useful just to test if the structure is correct).
`rpcclient.BiRPCConector` has been replaced with `context.ClientConnector`,
which is now passed alongside `context.Context` within the same struct
(`cgrates/birpc/context.Context`). Consequently, functions that were
previously relying on it are now receiving the context instead. The
changes were made in the following functions:
- `engine/connmanager.go` - `*ConnManager.Call`
- `engine/connmanager.go` - `*ConnManager.getConn`
- `engine/connmanager.go` - `*ConnManager.getConnWithConfig`
- `engine/libengine.go` - `NewRPCPool`
- `engine/libengine.go` - `NewRPCConnection`
- `agents/libagents.go` - `processRequest`
Compilation errors related to the `rpcclient.NewRPCClient` function were
resolved by adding the missing `context`, `max_reconnect_interval`, and
`delayFunc` parameters. Additionally, context was added to all calls made
by the client. An effort was made to avoid passing hardcoded values as
much as possible, and extra flags were added where necessary for cgr
binaries.
The `max_reconnect_interval` parameter is now passed from parent
functions, which required adjustments to the function signature.
A new context field was added to all agent objects to ensure access to
it before sending it to the `connmanager's Call`, effectively replacing
`birpcclient`. Although an alternative would have been to create the
new service and add it to the context right before passing it to the
handlers, the chosen approach is definitely more comfortable.
With the addition of a context field for the SIP servers agents, an
additional error needed to be handled, coming from the creation of the
service. Agent constructors within the services package log errors as
they occur and return. Alternate solutions considered were either
shutting down the engine instead of returning, or just logging the
occurrence as a warning, particularly when the `ctx.Client` isn't
required, especially in cases where bidirectional connections are not
needed. For the latter option, it's crucial to return the object with
the error rather than nil or to make the error nil immediately after
logging.
Context has been integrated into all internal Call implementations to
ensure the objects conform to the `birpc.ClientConnector` interface.
These implementations will be removed in the near future as all service
objects are being wrapped in a `birpc.Service` type that satisfies the
`birpc.ClientConnector` interface. Currently, they are being retained
as a reference in case of any unexpected issues that arise.
Ensured that the `birpc.Service` wrapped service objects are passed to
the internal channel getters rather than the objects themselves.
Add context.TODO() to all \*ConnManager.Call function calls. To be
replaced with the context passed to the Method, when available.
For all `*ConnManager.Call` function calls, `context.TODO()` has been
added. This will be replaced with the context passed to the method when
it becomes available.
The value returned by StringGetOpts is now passed directly to the
FirstNonEmpty function, instead of being assigned to a variable
first.
The implementation of the `*AnalyzerService.GetInternalBiRPCCodec`
function has been removed from the services package. Additionally,
the AnalyzerBiRPCConnector type definition and its associated methods
have been removed.
The codec implementation has been revised to include the following
changes:
- `rpc.ServerCodec` -> `birpc.ServerCodec`;
- `rpc2.ServerCodec` -> `birpc.BirpcCodec`;
- `rpc2.Request` -> `birpc.Request`;
- `rpc2.Response` -> `birpc.Response`;
- The constructors for json and gob birpc codecs in `cenkalti/rpc`
have been replaced with ones from the `birpc/jsonrpc` library;
- The gob codec implementation has been removed in favor of the
version already implemented in the birpc external library.
The server implementation has been updated with the following changes:
- A field that represents a simple RPC server has been added to the
Server struct;
- Both the simple and bidirectional RPC servers are now initialized
inside the Server constructor, eliminating the need for nil checks;
- Usage of `net/rpc` and `cenkalti/rpc2` has been replaced with
`cgrates/birpc`;
- Additional `(Bi)RPCUnregisterName` methods have been added;
- The implementations for (bi)json/gob servers have been somewhat
simplified.
Before deleting the Call functions and using the `birpc.NewService`
method to register the methods for all cgrates components, update the
Call functions to satisfy the `birpc.ClientConnector` interface. This
way it will be a bit safer. Had to be done for SessionS though.
The `BiRPCCall` method has been removed from coreutils.go. The
`RPCCall` and `APIerRPCCall` methods are also to be removed in the
future.
Ensured that all methods for `SessionSv1` and `SessionS` have the
correct function signature with context. The same adjustments were made
for the session dispatcher methods and for the `SessionSv1Interface`.
Also removed sessionsbirpc.go and smgbirpc.go files.
Implemented the following methods to help with the registration of
methods across all subsystems:
- `NewServiceWithName`;
- `NewDispatcherService` for all dispatcher methods;
- `NewService` for the remaining methods that are already named
correctly.
Compared to the constructor from the external library, these also make
sure that the naming of the methods is consistent with our constants.
Added context to the Call methods for the mock client connectors (used
in tests).
Removed unused rpc fields from inside the following services:
- EeS
- LoaderS
- ResourceS
- RouteS
- StatS
- ThresholdS
- SessionS
- CoreS
Updated the methods implementing the logic for API methods to align
with the latest changes, ensuring consistency and correctness. The
modifications include:
- Adjusting the function signature to the new format
(ctx, args, reply).
- Prefixing names with 'V*' to indicate that they are utilized by
or registered as APIs.
- Containing the complete logic within the methods, enabling APIs
to call them and return their reply directly.
The subsystems affected by these changes are detailed as follows:
- CoreS: Additional methods were implementing utilizing the
existing ones. Though modifying them directly was possible, certain
methods (e.g., StopCPUProfiling()) were used elsewhere and not as
RPC requests.
- CDRs: Renamed V1CountCDRs to V1GetCDRsCount.
- StatS: V1GetQueueFloatMetrics, V1GetQueueStringMetrics,
V1GetStatQueue accept different arguments compared to API functions
(opted to register StatSv1 instead).
- ResourceS: Renamed V1ResourcesForEvent to V1GetResourcesForEvent
to align with API naming.
- DispatcherS: Renamed V1GetProfilesForEvent to
DispatcherSv1GetProfilesForEvent.
- For the rest, adding context to the function signature was enough.
In the unit tests, wrapping the object within a biprc.Service is now
ensured before passing it to the internal connections map under the
corresponding key.
Some tests that are covering error cases, are also checking the other
return value besides the error. That check has been removed since it
is redundant.
Revised the RPC/BiRPC clients' constructors (for use in tests)
A different approach has been chosen for the handling of ping methods
within subsystems. Instead of defining the same structure in every file,
the ping methods were added inside the Service constructor function.
Though the existing Ping methods were left as they were, they will be
removed in the future.
An additional method has been implemented to register the Ping method
from outside of the engine package.
Implemented Sleep and CapsError methods for SessionS (before they were
exclusively for bidirectional use, I believe).
A specific issue has been fixed within the CapsError SessionSv1 API
implementation, which is designed to overwrite methods that cannot be
allocated due to the threshold limit being reached. Previously, it was
deallocating when writing the response, even when a spot hadn't been
allocated in the first place (due to the cap being hit). The reason
behind this, especially why the test was passing before, still needs
to be looked into, as the problem should have occurred from before.
Implement `*SessionSv1.RegisterInternalBiJSONConn` method in apier.
All agent methods have been registered under the SessionSv1 name. For
the correct method names, the leading "V1" prefix has been trimmed
using the `birpc.NewServiceWithMethodsRename` function.
Revise the RegisterRpcParams function to populate the parameters
while relying on the `*birpc.Service` type instead. This will
automatically also deal with the validation. At the moment,
any error encountered is logged without being returned. Might
be changed in the future.
Inside the cgrRPCAction function, `mapstructure.Decode`'s output parameter
is now guaranteed to always be a pointer.
Updated go.mod and go.sum.
Fixed some typos.
1120 lines
29 KiB
Go
1120 lines
29 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 utils
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/gob"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
math_rand "math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cgrates/rpcclient"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
var (
|
|
startCGRateSTime time.Time
|
|
boolGenerator *boolGen
|
|
|
|
rfc3339Rule = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.+$`)
|
|
sqlRule = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$`)
|
|
utcFormat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}[T]\d{2}:\d{2}:\d{2}$`)
|
|
gotimeRule = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.?\d*\s[+,-]\d+\s\w+$`)
|
|
gotimeRule2 = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.?\d*\s[+,-]\d+\s[+,-]\d+$`)
|
|
fsTimestamp = regexp.MustCompile(`^\d{16}$`)
|
|
astTimestamp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d*[+,-]\d+$`)
|
|
unixTimestampRule = regexp.MustCompile(`^\d{10}$`)
|
|
unixTimestampMilisecondsRule = regexp.MustCompile(`^\d{13}$`)
|
|
unixTimestampNanosecondsRule = regexp.MustCompile(`^\d{19}$`)
|
|
oneLineTimestampRule = regexp.MustCompile(`^\d{14}$`)
|
|
oneSpaceTimestampRule = regexp.MustCompile(`^\d{2}\.\d{2}.\d{4}\s{1}\d{2}:\d{2}:\d{2}$`)
|
|
eamonTimestampRule = regexp.MustCompile(`^\d{2}/\d{2}/\d{4}\s{1}\d{2}:\d{2}:\d{2}$`)
|
|
broadsoftTimestampRule = regexp.MustCompile(`^\d{14}\.\d{3}`)
|
|
)
|
|
|
|
func init() {
|
|
startCGRateSTime = time.Now()
|
|
math_rand.Seed(startCGRateSTime.UnixNano())
|
|
boolGenerator = newBoolGen()
|
|
}
|
|
|
|
// GetStartTime return the Start time of engine (in UNIX format)
|
|
func GetStartTime() string {
|
|
return startCGRateSTime.Format(time.UnixDate)
|
|
}
|
|
|
|
// BoolGenerator return the boolean generator
|
|
func BoolGenerator() *boolGen {
|
|
return boolGenerator
|
|
}
|
|
|
|
func NewCounter(start, limit int64) *Counter {
|
|
return &Counter{
|
|
value: start,
|
|
limit: limit,
|
|
}
|
|
}
|
|
|
|
type Counter struct {
|
|
value, limit int64
|
|
sync.Mutex
|
|
}
|
|
|
|
func (c *Counter) Next() int64 {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.value += 1
|
|
if c.limit > 0 && c.value > c.limit {
|
|
c.value = 0
|
|
}
|
|
return c.value
|
|
}
|
|
|
|
func (c *Counter) Value() int64 {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.value
|
|
}
|
|
|
|
// Returns first non empty string out of vals. Useful to extract defaults
|
|
func FirstNonEmpty(vals ...string) string {
|
|
for _, val := range vals {
|
|
if len(val) != 0 {
|
|
return val
|
|
}
|
|
}
|
|
return EmptyString
|
|
}
|
|
|
|
func FirstIntNonEmpty(vals ...int) int {
|
|
for _, val := range vals {
|
|
if val != 0 {
|
|
return val
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func FirstDurationNonEmpty(vals ...time.Duration) time.Duration {
|
|
for _, val := range vals {
|
|
if val != 0 {
|
|
return val
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Sha1 generate the SHA1 hash from any string
|
|
// the order of string matters
|
|
func Sha1(attrs ...string) string {
|
|
hasher := sha1.New()
|
|
for _, attr := range attrs {
|
|
hasher.Write([]byte(attr))
|
|
}
|
|
return fmt.Sprintf("%x", hasher.Sum(nil))
|
|
}
|
|
|
|
// helper function for uuid generation
|
|
func GenUUID() string {
|
|
b := make([]byte, 16)
|
|
io.ReadFull(rand.Reader, b)
|
|
b[6] = (b[6] & 0x0F) | 0x40
|
|
b[8] = (b[8] &^ 0x40) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[:4], b[4:6], b[6:8], b[8:10],
|
|
b[10:])
|
|
}
|
|
|
|
// UUIDSha1Prefix generates a prefix of the sha1 applied to an UUID
|
|
// prefix 8 is chosen since the probability of colision starts being minimal after 7 characters (see git commits)
|
|
func UUIDSha1Prefix() string {
|
|
return Sha1(GenUUID())[:7]
|
|
}
|
|
|
|
// Round return rounded version of x with prec precision.
|
|
//
|
|
// Special cases are:
|
|
//
|
|
// Round(±0) = ±0
|
|
// Round(±Inf) = ±Inf
|
|
// Round(NaN) = NaN
|
|
func Round(x float64, prec int, method string) float64 {
|
|
var rounder float64
|
|
maxPrec := 7 // define a max precision to cut float errors
|
|
if maxPrec < prec {
|
|
maxPrec = prec
|
|
}
|
|
pow := math.Pow(10, float64(prec))
|
|
intermed := x * pow
|
|
_, frac := math.Modf(intermed)
|
|
|
|
switch method {
|
|
case MetaRoundingUp:
|
|
if frac >= math.Pow10(-maxPrec) { // Max precision we go, rest is float chaos
|
|
rounder = math.Ceil(intermed)
|
|
} else {
|
|
rounder = math.Floor(intermed)
|
|
}
|
|
case MetaRoundingDown:
|
|
rounder = math.Floor(intermed)
|
|
case MetaRoundingMiddle:
|
|
if frac >= 0.5 {
|
|
rounder = math.Ceil(intermed)
|
|
} else {
|
|
rounder = math.Floor(intermed)
|
|
}
|
|
default:
|
|
rounder = intermed
|
|
}
|
|
|
|
return rounder / pow
|
|
}
|
|
|
|
// RoundStatDuration is used in engine package for stat metrics that has duration (e.g acd metric, tcd metric, etc...)
|
|
func RoundStatDuration(x time.Duration, prec int) time.Duration {
|
|
return x.Round(time.Duration(math.Pow10(9 - prec)))
|
|
}
|
|
|
|
func getAddDuration(tmStr string) (addDur time.Duration, err error) {
|
|
eDurIdx := strings.Index(tmStr, "+")
|
|
if eDurIdx == -1 {
|
|
return
|
|
}
|
|
return time.ParseDuration(tmStr[eDurIdx+1:])
|
|
}
|
|
|
|
// ParseTimeDetectLayout returns the time from string
|
|
func ParseTimeDetectLayout(tmStr string, timezone string) (time.Time, error) {
|
|
tmStr = strings.TrimSpace(tmStr)
|
|
var nilTime time.Time
|
|
if len(tmStr) == 0 || tmStr == MetaUnlimited {
|
|
return nilTime, nil
|
|
}
|
|
loc, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
return nilTime, err
|
|
}
|
|
switch {
|
|
case tmStr == MetaUnlimited || tmStr == "":
|
|
// leave it at zero
|
|
case tmStr == "*daily":
|
|
return time.Now().AddDate(0, 0, 1), nil // add one day
|
|
case tmStr == "*monthly":
|
|
return time.Now().AddDate(0, 1, 0), nil // add one month
|
|
case tmStr == "*monthly_estimated":
|
|
return monthlyEstimated(time.Now())
|
|
case tmStr == "*yearly":
|
|
return time.Now().AddDate(1, 0, 0), nil // add one year
|
|
|
|
case strings.HasPrefix(tmStr, "*month_end"):
|
|
expDate := GetEndOfMonth(time.Now())
|
|
extraDur, err := getAddDuration(tmStr)
|
|
if err != nil {
|
|
return nilTime, err
|
|
}
|
|
expDate = expDate.Add(extraDur)
|
|
return expDate, nil
|
|
case strings.HasPrefix(tmStr, "*mo"): // add one month and extra duration
|
|
extraDur, err := getAddDuration(tmStr)
|
|
if err != nil {
|
|
return nilTime, err
|
|
}
|
|
return time.Now().AddDate(0, 1, 0).Add(extraDur), nil
|
|
case astTimestamp.MatchString(tmStr):
|
|
return time.Parse("2006-01-02T15:04:05.999999999-0700", tmStr)
|
|
case rfc3339Rule.MatchString(tmStr):
|
|
return time.Parse(time.RFC3339, tmStr)
|
|
case gotimeRule.MatchString(tmStr):
|
|
return time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", tmStr)
|
|
case gotimeRule2.MatchString(tmStr):
|
|
return time.Parse("2006-01-02 15:04:05.999999999 -0700 -0700", tmStr)
|
|
case sqlRule.MatchString(tmStr):
|
|
return time.ParseInLocation("2006-01-02 15:04:05", tmStr, loc)
|
|
case fsTimestamp.MatchString(tmStr):
|
|
if tmstmp, err := strconv.ParseInt(tmStr+"000", 10, 64); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Unix(0, tmstmp).In(loc), nil
|
|
}
|
|
case unixTimestampRule.MatchString(tmStr):
|
|
//error never happens because of regex
|
|
tmstmp, _ := strconv.ParseInt(tmStr, 10, 64)
|
|
return time.Unix(tmstmp, 0).In(loc), nil
|
|
case unixTimestampMilisecondsRule.MatchString(tmStr):
|
|
//error never happens because of regex
|
|
tmstmp, _ := strconv.ParseInt(tmStr, 10, 64)
|
|
return time.Unix(0, tmstmp*int64(time.Millisecond)).In(loc), nil
|
|
|
|
case unixTimestampNanosecondsRule.MatchString(tmStr):
|
|
if tmstmp, err := strconv.ParseInt(tmStr, 10, 64); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Unix(0, tmstmp).In(loc), nil
|
|
}
|
|
case tmStr == "0" || len(tmStr) == 0: // Time probably missing from request
|
|
return nilTime, nil
|
|
case oneLineTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("20060102150405", tmStr, loc)
|
|
case oneSpaceTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("02.01.2006 15:04:05", tmStr, loc)
|
|
case eamonTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("02/01/2006 15:04:05", tmStr, loc)
|
|
case broadsoftTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("20060102150405.999", tmStr, loc)
|
|
case tmStr == MetaNow:
|
|
return time.Now(), nil
|
|
case strings.HasPrefix(tmStr, "+"):
|
|
tmStr = strings.TrimPrefix(tmStr, "+")
|
|
if tmStrTmp, err := time.ParseDuration(tmStr); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Now().Add(tmStrTmp), nil
|
|
}
|
|
case utcFormat.MatchString(tmStr):
|
|
return time.ParseInLocation("2006-01-02T15:04:05", tmStr, loc)
|
|
|
|
}
|
|
return nilTime, errors.New("Unsupported time format")
|
|
}
|
|
|
|
func monthlyEstimated(t1 time.Time) (time.Time, error) {
|
|
initialMnt := t1.Month()
|
|
tAfter := t1.AddDate(0, 1, 0)
|
|
for tAfter.Month()-initialMnt > 1 {
|
|
tAfter = tAfter.AddDate(0, 0, -1)
|
|
}
|
|
return tAfter, nil
|
|
}
|
|
|
|
// RoundDuration returns a number equal or larger than the amount that exactly
|
|
// is divisible to whole
|
|
func RoundDuration(whole, amount time.Duration) time.Duration {
|
|
a, w := float64(amount), float64(whole)
|
|
if math.Mod(a, w) == 0 {
|
|
return amount
|
|
}
|
|
return time.Duration((w - math.Mod(a, w)) + a)
|
|
}
|
|
|
|
func SplitPrefix(prefix string, minLength int) []string {
|
|
length := int(math.Max(float64(len(prefix)-(minLength-1)), 0))
|
|
subs := make([]string, length)
|
|
max := len(prefix)
|
|
for i := 0; i < length; i++ {
|
|
subs[i] = prefix[:max-i]
|
|
}
|
|
return subs
|
|
}
|
|
|
|
func SplitSuffix(suffix string) []string {
|
|
length := len(suffix)
|
|
subs := make([]string, length)
|
|
max := len(suffix) - 1
|
|
for i := 0; i < length; i++ {
|
|
subs[i] = suffix[max-i:]
|
|
}
|
|
return subs
|
|
}
|
|
|
|
func CopyHour(src, dest time.Time) time.Time {
|
|
if src.Hour() == 0 && src.Minute() == 0 && src.Second() == 0 {
|
|
return src
|
|
}
|
|
return time.Date(dest.Year(), dest.Month(), dest.Day(), src.Hour(), src.Minute(), src.Second(), src.Nanosecond(), src.Location())
|
|
}
|
|
|
|
// Parses duration, considers s as time unit if not provided, seconds as float to specify subunits
|
|
func ParseDurationWithSecs(durStr string) (d time.Duration, err error) {
|
|
if durStr == "" {
|
|
return
|
|
}
|
|
if _, err = strconv.ParseFloat(durStr, 64); err == nil { // Seconds format considered
|
|
durStr += "s"
|
|
}
|
|
return time.ParseDuration(durStr)
|
|
}
|
|
|
|
// Parses duration, considers s as time unit if not provided, seconds as float to specify subunits
|
|
func ParseDurationWithNanosecs(durStr string) (d time.Duration, err error) {
|
|
if durStr == "" {
|
|
return
|
|
}
|
|
if durStr == MetaUnlimited {
|
|
durStr = "-1"
|
|
}
|
|
if _, err = strconv.ParseFloat(durStr, 64); err == nil { // Seconds format considered
|
|
durStr += "ns"
|
|
}
|
|
return time.ParseDuration(durStr)
|
|
}
|
|
|
|
// returns the minimum duration between the two
|
|
func MinDuration(d1, d2 time.Duration) time.Duration {
|
|
if d1 < d2 {
|
|
return d1
|
|
}
|
|
return d2
|
|
}
|
|
|
|
// ParseZeroRatingSubject will parse the subject in the balance
|
|
// returns duration if able to extract it from subject
|
|
// returns error if not able to parse duration (ie: if ratingSubject is standard one)
|
|
func ParseZeroRatingSubject(tor, rateSubj string, defaultRateSubj map[string]string, isUnitBal bool) (time.Duration, error) {
|
|
rateSubj = strings.TrimSpace(rateSubj)
|
|
if !isUnitBal && rateSubj == EmptyString {
|
|
return 0, errors.New("no rating subject for monetary")
|
|
}
|
|
if rateSubj == EmptyString ||
|
|
rateSubj == MetaAny {
|
|
var hasToR bool
|
|
if rateSubj, hasToR = defaultRateSubj[tor]; !hasToR {
|
|
rateSubj = defaultRateSubj[MetaAny]
|
|
}
|
|
}
|
|
if !strings.HasPrefix(rateSubj, MetaRatingSubjectPrefix) {
|
|
return 0, errors.New("malformed rating subject: " + rateSubj)
|
|
}
|
|
durStr := rateSubj[len(MetaRatingSubjectPrefix):]
|
|
if val, err := strconv.ParseFloat(durStr, 64); err == nil { // No time unit, postpend
|
|
return time.Duration(val), nil // just return the float value converted(this should be faster than reparsing the string)
|
|
}
|
|
return time.ParseDuration(durStr)
|
|
}
|
|
|
|
func ConcatenatedKey(keyVals ...string) string {
|
|
return strings.Join(keyVals, ConcatenatedKeySep)
|
|
}
|
|
|
|
func SplitConcatenatedKey(key string) []string {
|
|
return strings.Split(key, ConcatenatedKeySep)
|
|
}
|
|
|
|
func InfieldJoin(vals ...string) string {
|
|
return strings.Join(vals, InfieldSep)
|
|
}
|
|
|
|
func InfieldSplit(val string) []string {
|
|
return strings.Split(val, InfieldSep)
|
|
}
|
|
|
|
// Splited Unzip in small functions to have better coverage
|
|
func Unzip(src, dest string) error {
|
|
r, err := zip.OpenReader(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
for _, f := range r.File {
|
|
path := filepath.Join(dest, f.Name)
|
|
if f.FileInfo().IsDir() {
|
|
os.MkdirAll(path, f.Mode())
|
|
continue
|
|
}
|
|
err = unzipFile(f, path, f.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
type zipFile interface {
|
|
Open() (io.ReadCloser, error)
|
|
}
|
|
|
|
func unzipFile(f zipFile, path string, fm os.FileMode) (err error) {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = copyFile(rc, path, fm)
|
|
rc.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyFile(rc io.ReadCloser, path string, fm os.FileMode) (err error) {
|
|
f, err := os.OpenFile(
|
|
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = io.Copy(f, rc)
|
|
return
|
|
}
|
|
|
|
// Fib returns successive Fibonacci numbers.
|
|
func Fib() func() int {
|
|
a, b := 0, 1
|
|
return func() int {
|
|
if b > 0 { // only increment Fibonacci numbers while b doesn't overflow
|
|
a, b = b, a+b
|
|
}
|
|
return a
|
|
}
|
|
}
|
|
|
|
// FibDuration returns successive Fibonacci numbers as time.Duration with the
|
|
// unit specified by durationUnit or maxDuration if it is exceeded
|
|
func FibDuration(durationUnit, maxDuration time.Duration) func() time.Duration {
|
|
fib := Fib()
|
|
return func() time.Duration {
|
|
fibNrAsDuration := time.Duration(fib())
|
|
if fibNrAsDuration > (AbsoluteMaxDuration / durationUnit) { // check if the current fibonacci nr. in the sequence would exceed the absolute maximum duration if multiplied by the duration unit value
|
|
fibNrAsDuration = AbsoluteMaxDuration
|
|
} else {
|
|
fibNrAsDuration *= durationUnit
|
|
}
|
|
if maxDuration > 0 && maxDuration < fibNrAsDuration {
|
|
return maxDuration
|
|
}
|
|
return fibNrAsDuration
|
|
}
|
|
}
|
|
|
|
// Utilities to provide pointers where we need to define ad-hoc
|
|
func StringPointer(str string) *string {
|
|
if str == MetaZero {
|
|
str = EmptyString
|
|
return &str
|
|
}
|
|
return &str
|
|
}
|
|
|
|
func IntPointer(i int) *int {
|
|
return &i
|
|
}
|
|
|
|
func Int64Pointer(i int64) *int64 {
|
|
return &i
|
|
}
|
|
|
|
func Float64Pointer(f float64) *float64 {
|
|
return &f
|
|
}
|
|
|
|
func BoolPointer(b bool) *bool {
|
|
return &b
|
|
}
|
|
|
|
func StringMapPointer(sm StringMap) *StringMap {
|
|
return &sm
|
|
}
|
|
|
|
func MapStringStringPointer(mp map[string]string) *map[string]string {
|
|
return &mp
|
|
}
|
|
|
|
func TimePointer(t time.Time) *time.Time {
|
|
return &t
|
|
}
|
|
|
|
func DurationPointer(d time.Duration) *time.Duration {
|
|
return &d
|
|
}
|
|
|
|
func SliceStringPointer(d []string) *[]string {
|
|
return &d
|
|
}
|
|
|
|
func ToIJSON(v any) string {
|
|
b, _ := json.MarshalIndent(v, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
func ToJSON(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
// Simple object cloner, b should be a pointer towards a value into which we want to decode
|
|
func Clone(a, b any) error {
|
|
buff := new(bytes.Buffer)
|
|
enc := gob.NewEncoder(buff)
|
|
dec := gob.NewDecoder(buff)
|
|
if err := enc.Encode(a); err != nil {
|
|
return err
|
|
}
|
|
return dec.Decode(b)
|
|
}
|
|
|
|
// Used as generic function logic for various fields
|
|
|
|
// Attributes
|
|
//
|
|
// source - the base source
|
|
// width - the field width
|
|
// strip - if present it will specify the strip strategy, when missing strip will not be allowed
|
|
// padding - if present it will specify the padding strategy to use, left, right, zeroleft, zeroright
|
|
func FmtFieldWidth(fieldID, source string, width int, strip, padding string, mandatory bool) (string, error) {
|
|
if mandatory && len(source) == 0 {
|
|
return "", fmt.Errorf("Empty source value for fieldID: <%s>", fieldID)
|
|
}
|
|
if width == 0 { // Disable width processing if not defined
|
|
return source, nil
|
|
}
|
|
if len(source) == width { // the source is exactly the maximum length
|
|
return source, nil
|
|
}
|
|
if len(source) > width { //the source is bigger than allowed
|
|
if len(strip) == 0 {
|
|
return "", fmt.Errorf("Source %s is bigger than the width %d, no strip defied, fieldID: <%s>", source, width, fieldID)
|
|
}
|
|
if strip == MetaRight {
|
|
return source[:width], nil
|
|
} else if strip == MetaXRight {
|
|
return source[:width-1] + "x", nil // Suffix with x to mark prefix
|
|
} else if strip == MetaLeft {
|
|
diffIndx := len(source) - width
|
|
return source[diffIndx:], nil
|
|
} else if strip == MetaXLeft { // Prefix one x to mark stripping
|
|
diffIndx := len(source) - width
|
|
return "x" + source[diffIndx+1:], nil
|
|
}
|
|
} else { //the source is smaller as the maximum allowed
|
|
if len(padding) == 0 {
|
|
return "", fmt.Errorf("Source %s is smaller than the width %d, no padding defined, fieldID: <%s>", source, width, fieldID)
|
|
}
|
|
var paddingFmt string
|
|
switch padding {
|
|
case MetaRight:
|
|
paddingFmt = fmt.Sprintf("%%-%ds", width)
|
|
case MetaLeft:
|
|
paddingFmt = fmt.Sprintf("%%%ds", width)
|
|
case MetaZeroLeft:
|
|
paddingFmt = fmt.Sprintf("%%0%ds", width)
|
|
}
|
|
if len(paddingFmt) != 0 {
|
|
return fmt.Sprintf(paddingFmt, source), nil
|
|
}
|
|
}
|
|
return source, nil
|
|
}
|
|
|
|
func GetEndOfMonth(ref time.Time) time.Time {
|
|
if ref.IsZero() {
|
|
return time.Now()
|
|
}
|
|
year, month, _ := ref.Date()
|
|
if month == time.December {
|
|
year++
|
|
month = time.January
|
|
} else {
|
|
month++
|
|
}
|
|
eom := time.Date(year, month, 1, 0, 0, 0, 0, ref.Location())
|
|
return eom.Add(-time.Second)
|
|
}
|
|
|
|
// formats number in K,M,G, etc.
|
|
func SizeFmt(num float64, suffix string) string {
|
|
if suffix == EmptyString {
|
|
suffix = "B"
|
|
}
|
|
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} {
|
|
if math.Abs(num) < 1024.0 {
|
|
return fmt.Sprintf("%3.1f%s%s", num, unit, suffix)
|
|
}
|
|
num /= 1024.0
|
|
}
|
|
return fmt.Sprintf("%.1f%s%s", num, "Yi", suffix)
|
|
}
|
|
|
|
func TimeIs0h(t time.Time) bool {
|
|
return t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0
|
|
}
|
|
|
|
func ParseHierarchyPath(path string, sep string) HierarchyPath {
|
|
if sep == EmptyString {
|
|
for _, sep = range []string{"/", NestingSep} {
|
|
if idx := strings.Index(path, sep); idx != -1 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
path = strings.Trim(path, sep) // Need to strip if prefix of suffiy (eg: paths with /) so we can properly split
|
|
return HierarchyPath(strings.Split(path, sep))
|
|
}
|
|
|
|
// HierarchyPath is used in various places to represent various path hierarchies (eg: in Diameter groups, XML trees)
|
|
type HierarchyPath []string
|
|
|
|
func (h HierarchyPath) AsString(sep string, prefix bool) string {
|
|
if len(h) == 0 {
|
|
return EmptyString
|
|
}
|
|
retStr := EmptyString
|
|
for idx, itm := range h {
|
|
if idx == 0 {
|
|
if prefix {
|
|
retStr += sep
|
|
}
|
|
} else {
|
|
retStr += sep
|
|
}
|
|
retStr += itm
|
|
}
|
|
return retStr
|
|
}
|
|
|
|
// Clone returns a deep copy of HierarchyPath
|
|
func (h HierarchyPath) Clone() (cln HierarchyPath) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
return CloneStringSlice(h)
|
|
}
|
|
|
|
// Mask a number of characters in the suffix of the destination
|
|
func MaskSuffix(dest string, maskLen int) string {
|
|
destLen := len(dest)
|
|
if maskLen < 0 {
|
|
return dest
|
|
} else if maskLen > destLen {
|
|
maskLen = destLen
|
|
}
|
|
dest = dest[:destLen-maskLen]
|
|
for i := 0; i < maskLen; i++ {
|
|
dest += MaskChar
|
|
}
|
|
return dest
|
|
}
|
|
|
|
// Sortable Int64Slice
|
|
type Int64Slice []int64
|
|
|
|
func (slc Int64Slice) Len() int {
|
|
return len(slc)
|
|
}
|
|
func (slc Int64Slice) Swap(i, j int) {
|
|
slc[i], slc[j] = slc[j], slc[i]
|
|
}
|
|
func (slc Int64Slice) Less(i, j int) bool {
|
|
return slc[i] < slc[j]
|
|
}
|
|
|
|
func GetCGRVersion() (vers string, err error) {
|
|
vers = fmt.Sprintf("%s@%s", CGRateS, Version)
|
|
if GitLastLog == "" {
|
|
return vers, nil
|
|
}
|
|
rdr := bytes.NewBufferString(GitLastLog)
|
|
var commitHash string
|
|
var commitDate time.Time
|
|
for i := 0; i < 5; i++ { // read a maximum of 5 lines
|
|
var ln string
|
|
ln, err = rdr.ReadString('\n')
|
|
if err != nil {
|
|
return vers, fmt.Errorf("Building version - error: <%s> reading line from file", err.Error()) //or errorsNew()
|
|
}
|
|
if strings.HasPrefix(ln, "commit ") {
|
|
commitSplt := strings.Split(ln, " ")
|
|
if len(commitSplt) != 2 {
|
|
return vers, fmt.Errorf("Building version - cannot extract commit hash")
|
|
}
|
|
commitHash = commitSplt[1]
|
|
continue
|
|
}
|
|
if strings.HasPrefix(ln, "CommitDate:") {
|
|
dateSplt := strings.Split(ln, ": ")
|
|
if len(dateSplt) != 2 {
|
|
return vers, fmt.Errorf("Building version - cannot split commit date")
|
|
}
|
|
commitDate, err = time.Parse("Mon Jan 2 15:04:05 2006 -0700", strings.TrimSpace(dateSplt[1]))
|
|
if err != nil {
|
|
return vers, fmt.Errorf("Building version - error: <%s> compiling commit date", err.Error())
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if commitHash == "" || commitDate.IsZero() {
|
|
return vers, fmt.Errorf("Cannot find commitHash or commitDate information")
|
|
}
|
|
//CGRateS@v0.11.0~dev-20200110075344-7572e7b11e00
|
|
return fmt.Sprintf("%s@%s-%s-%s", CGRateS, Version, commitDate.UTC().Format("20060102150405"), commitHash[:12]), nil
|
|
}
|
|
|
|
func NewTenantID(tntID string) *TenantID {
|
|
if !strings.Contains(tntID, ConcatenatedKeySep) { // no :, ID without Tenant
|
|
return &TenantID{ID: tntID}
|
|
}
|
|
tIDSplt := strings.SplitN(tntID, ConcatenatedKeySep, 2)
|
|
return &TenantID{Tenant: tIDSplt[0], ID: tIDSplt[1]}
|
|
}
|
|
|
|
type PaginatorWithTenant struct {
|
|
Tenant string
|
|
Paginator
|
|
}
|
|
|
|
type TenantWithAPIOpts struct {
|
|
Tenant string
|
|
APIOpts map[string]any
|
|
}
|
|
|
|
type MemoryPrf struct {
|
|
Tenant string
|
|
DirPath string
|
|
Interval time.Duration
|
|
NrFiles int
|
|
APIOpts map[string]any
|
|
}
|
|
|
|
type TenantID struct {
|
|
Tenant string
|
|
ID string
|
|
}
|
|
|
|
type TenantIDWithAPIOpts struct {
|
|
*TenantID
|
|
APIOpts map[string]any
|
|
}
|
|
|
|
func (tID *TenantID) TenantID() string {
|
|
return ConcatenatedKey(tID.Tenant, tID.ID)
|
|
}
|
|
|
|
func (tID *TenantIDWithAPIOpts) TenantIDConcatenated() string {
|
|
return ConcatenatedKey(tID.Tenant, tID.ID)
|
|
}
|
|
|
|
// RPCCall is a generic method calling RPC on a struct instance
|
|
// serviceMethod is assumed to be in the form InstanceV1.Method
|
|
// where V1Method will become RPC method called on instance
|
|
func RPCCall(inst any, serviceMethod string, args any, reply any) error {
|
|
methodSplit := strings.Split(serviceMethod, ".")
|
|
if len(methodSplit) != 2 {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
method := reflect.ValueOf(inst).MethodByName(
|
|
strings.ToUpper(methodSplit[0][len(methodSplit[0])-2:]) + methodSplit[1])
|
|
if !method.IsValid() {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
params := []reflect.Value{reflect.ValueOf(args), reflect.ValueOf(reply)}
|
|
ret := method.Call(params)
|
|
if len(ret) != 1 {
|
|
return ErrServerError
|
|
}
|
|
if ret[0].Interface() == nil {
|
|
return nil
|
|
}
|
|
err, ok := ret[0].Interface().(error)
|
|
if !ok {
|
|
return ErrServerError
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ApierRPCCall implements generic RPCCall for APIer instances
|
|
func APIerRPCCall(inst any, serviceMethod string, args any, reply any) error {
|
|
methodSplit := strings.Split(serviceMethod, ".")
|
|
if len(methodSplit) != 2 {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
method := reflect.ValueOf(inst).MethodByName(methodSplit[1])
|
|
if !method.IsValid() {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
params := []reflect.Value{reflect.ValueOf(args), reflect.ValueOf(reply)}
|
|
ret := method.Call(params)
|
|
if len(ret) != 1 {
|
|
return ErrServerError
|
|
}
|
|
if ret[0].Interface() == nil {
|
|
return nil
|
|
}
|
|
err, ok := ret[0].Interface().(error)
|
|
if !ok {
|
|
return ErrServerError
|
|
}
|
|
return err
|
|
}
|
|
|
|
// CachedRPCResponse is used to cache a RPC response
|
|
type CachedRPCResponse struct {
|
|
Result any
|
|
Error error
|
|
}
|
|
|
|
func ReverseString(s string) string {
|
|
r := []rune(s)
|
|
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
|
r[i], r[j] = r[j], r[i]
|
|
}
|
|
return string(r)
|
|
}
|
|
|
|
func GetUrlRawArguments(dialURL string) (out map[string]string) {
|
|
out = make(map[string]string)
|
|
idx := strings.IndexRune(dialURL, '?')
|
|
if idx == -1 {
|
|
return
|
|
}
|
|
strParams := dialURL[idx+1:]
|
|
if len(strParams) == 0 {
|
|
return
|
|
}
|
|
vecParams := strings.Split(strParams, "&")
|
|
for _, paramPair := range vecParams {
|
|
idx := strings.IndexRune(paramPair, '=')
|
|
if idx == -1 {
|
|
continue
|
|
}
|
|
out[paramPair[:idx]] = paramPair[idx+1:]
|
|
}
|
|
return
|
|
}
|
|
|
|
// WarnExecTime is used when we need to meassure the execution of specific functions
|
|
// and warn when the total duration is higher than expected
|
|
// should be usually called with defer, ie: defer WarnExecTime(time.Now(), "MyTestFunc", 2*time.Second)
|
|
func WarnExecTime(startTime time.Time, logID string, maxDur time.Duration) {
|
|
totalDur := time.Since(startTime)
|
|
if totalDur > maxDur {
|
|
Logger.Warning(fmt.Sprintf("<%s> execution took: <%s>", logID, totalDur))
|
|
}
|
|
}
|
|
|
|
// endchan := LongExecTimeDetector("mesaj", 5*time.Second)
|
|
// defer func() { close(endchan) }()
|
|
func LongExecTimeDetector(logID string, maxDur time.Duration) (endchan chan struct{}) {
|
|
endchan = make(chan struct{}, 1)
|
|
go func() {
|
|
select {
|
|
case <-time.After(maxDur):
|
|
Logger.Warning(fmt.Sprintf("<%s> execution more than: <%s>", logID, maxDur))
|
|
case <-endchan:
|
|
}
|
|
}()
|
|
return
|
|
}
|
|
|
|
type StringWithAPIOpts struct {
|
|
APIOpts map[string]any
|
|
Tenant string
|
|
Arg string
|
|
}
|
|
|
|
func CastRPCErr(err error) error {
|
|
if err != nil {
|
|
if _, has := ErrMap[err.Error()]; has {
|
|
return ErrMap[err.Error()]
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RandomInteger returns a random 64-bit integer between min and max values
|
|
func RandomInteger(min, max int64) int64 {
|
|
return math_rand.Int63n(max-min) + min
|
|
}
|
|
|
|
type LoadIDsWithAPIOpts struct {
|
|
LoadIDs map[string]int64
|
|
Tenant string
|
|
APIOpts map[string]any
|
|
}
|
|
|
|
// IsURL returns if the path is an URL
|
|
func IsURL(path string) bool {
|
|
return strings.HasPrefix(path, "https://") ||
|
|
strings.HasPrefix(path, "http://")
|
|
}
|
|
|
|
// GetIndexesArg the API argumets to specify an index
|
|
type GetIndexesArg struct {
|
|
IdxItmType string
|
|
TntCtx string
|
|
IdxKey string
|
|
Tenant string
|
|
APIOpts map[string]any
|
|
}
|
|
|
|
// SetIndexesArg the API arguments needed for seting an index
|
|
type SetIndexesArg struct {
|
|
IdxItmType string
|
|
TntCtx string
|
|
Indexes map[string]StringSet
|
|
Tenant string
|
|
APIOpts map[string]any
|
|
}
|
|
|
|
type DurationArgs struct {
|
|
Duration time.Duration
|
|
APIOpts map[string]any
|
|
Tenant string
|
|
}
|
|
|
|
type DirectoryArgs struct {
|
|
DirPath string
|
|
APIOpts map[string]any
|
|
Tenant string
|
|
}
|
|
|
|
// AESEncrypt will encrypt the provided txt using the encKey and AES algorithm
|
|
func AESEncrypt(txt, encKey string) (encrypted string, err error) {
|
|
key, _ := hex.DecodeString(encKey)
|
|
var blk cipher.Block
|
|
if blk, err = aes.NewCipher(key); err != nil {
|
|
return
|
|
}
|
|
var aesGCM cipher.AEAD
|
|
aesGCM, _ = cipher.NewGCM(blk)
|
|
nonce := make([]byte, aesGCM.NonceSize())
|
|
io.ReadFull(rand.Reader, nonce)
|
|
return fmt.Sprintf("%x", aesGCM.Seal(nonce, nonce, []byte(txt), nil)), nil
|
|
}
|
|
|
|
// AESDecrypt will decrypt the provided encrypted txt using the encKey and AES algorithm
|
|
func AESDecrypt(encrypted string, encKey string) (txt string, err error) {
|
|
|
|
key, _ := hex.DecodeString(encKey)
|
|
enc, _ := hex.DecodeString(encrypted)
|
|
|
|
var blk cipher.Block
|
|
if blk, err = aes.NewCipher(key); err != nil {
|
|
return
|
|
}
|
|
|
|
var aesGCM cipher.AEAD
|
|
aesGCM, _ = cipher.NewGCM(blk)
|
|
nonceSize := aesGCM.NonceSize()
|
|
nonce, ciphertext := enc[:nonceSize], enc[nonceSize:]
|
|
var plaintext []byte
|
|
plaintext, err = aesGCM.Open(nil, nonce, ciphertext, nil)
|
|
return string(plaintext), err
|
|
}
|
|
|
|
// Hash generates the hash text
|
|
func ComputeHash(dataKeys ...string) (lns string, err error) {
|
|
var hashByts []byte
|
|
hashByts, err = bcrypt.GenerateFromPassword(
|
|
[]byte(ConcatenatedKey(dataKeys...)),
|
|
bcrypt.MinCost)
|
|
return string(hashByts), err
|
|
}
|
|
|
|
// VerifyHash matches the data hash with the dataKeys ha
|
|
func VerifyHash(hash string, dataKeys ...string) bool {
|
|
err := bcrypt.CompareHashAndPassword([]byte(hash),
|
|
[]byte(ConcatenatedKey(dataKeys...)))
|
|
return err == nil
|
|
}
|
|
|
|
// newBoolGen initialize an efficient boolean generator
|
|
func newBoolGen() *boolGen {
|
|
return &boolGen{src: math_rand.NewSource(time.Now().UnixNano())}
|
|
}
|
|
|
|
// boolGen is an efficient boolean generator
|
|
type boolGen struct {
|
|
src math_rand.Source
|
|
cache int64
|
|
remaining int
|
|
}
|
|
|
|
// RandomBool generate a random boolean
|
|
func (b *boolGen) RandomBool() bool {
|
|
if b.remaining == 0 {
|
|
b.cache, b.remaining = b.src.Int63(), 63
|
|
}
|
|
result := b.cache&0x01 == 1
|
|
b.cache >>= 1
|
|
b.remaining--
|
|
return result
|
|
}
|
|
|
|
// GenerateDBItemOpts will create the options for DB replication
|
|
// if they are empty they should be omitted
|
|
func GenerateDBItemOpts(apiKey, routeID, cache, rmtHost string) (mp map[string]any) {
|
|
mp = make(map[string]any)
|
|
if apiKey != EmptyString {
|
|
mp[OptsAPIKey] = apiKey
|
|
}
|
|
if routeID != EmptyString {
|
|
mp[OptsRouteID] = routeID
|
|
}
|
|
if cache != EmptyString {
|
|
mp[CacheOpt] = cache
|
|
}
|
|
if rmtHost != EmptyString {
|
|
mp[RemoteHostOpt] = rmtHost
|
|
}
|
|
return
|
|
}
|
|
|
|
// SplitPath splits filter rules based on the specified separator
|
|
func SplitPath(rule string, sep byte, n int) (splt []string) {
|
|
var p, st int
|
|
if n <= 0 {
|
|
n = bytes.Count([]byte(rule), []byte{sep}) + 1
|
|
}
|
|
if n == 1 {
|
|
return []string{rule}
|
|
}
|
|
splt = make([]string, 0, n)
|
|
for i, b := range rule {
|
|
switch byte(b) {
|
|
case sep:
|
|
if p == 0 {
|
|
splt = append(splt, rule[st:i])
|
|
st = i + 1
|
|
if len(splt) == n-1 {
|
|
splt = append(splt, rule[st:])
|
|
return
|
|
}
|
|
}
|
|
case IdxStart[0]:
|
|
p++
|
|
case IdxEnd[0]:
|
|
p--
|
|
}
|
|
}
|
|
splt = append(splt, rule[st:])
|
|
return
|
|
}
|
|
|
|
type PanicMessageArgs struct {
|
|
Tenant string
|
|
APIOpts map[string]any
|
|
Message string
|
|
}
|