diff --git a/Makefile.conf.template b/Makefile.conf.template index 2ad5de067c3..bcc9d42f2c6 100644 --- a/Makefile.conf.template +++ b/Makefile.conf.template @@ -28,6 +28,7 @@ #identity= Adds support for SIP Identity (see RFC 4474). | SSL library, typically libssl #jabber= Integrates XODE XML parser for parsing Jabber messages | Expat library. #json= Introduces a new type of variable that provides both serialization and de-serialization from JSON format. | JSON library, libjson +#launch_darkly= Implements an interface to the Launch Darkly feature management cloud #ldap= Implements an LDAP search interface for OpenSIPS | OpenLDAP library & development files, typically libldap and libldap-dev #lua= Easily implement your own OpenSIPS extensions in Lua | liblua5.1-0-dev, libmemcache-dev and libmysqlclient-dev #httpd= Provides an HTTP transport layer implementation for OpenSIPS. | libmicrohttpd @@ -67,7 +68,7 @@ #xmpp= Gateway between OpenSIPS and a jabber server. It enables the exchange of IMs between SIP clients and XMPP(jabber) clients. | parsing/building XML files, typically libexpat1-devel #uuid= UUID generator | uuid-dev -exclude_modules?= aaa_diameter aaa_radius auth_jwt b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka h350 httpd identity jabber json ldap lua mi_xmlrpc_ng mmgeoip osp perl pi_http presence presence_dialoginfo presence_mwi presence_xml presence_dfks proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_usrloc pua_xmpp python regex rabbitmq rabbitmq_consumer rest_client rls siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp +exclude_modules?= aaa_diameter aaa_radius auth_jwt b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka h350 httpd identity jabber json launch_darkly ldap lua mi_xmlrpc_ng mmgeoip osp perl pi_http presence presence_dialoginfo presence_mwi presence_xml presence_dfks proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_usrloc pua_xmpp python regex rabbitmq rabbitmq_consumer rest_client rls siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp include_modules?= diff --git a/modules/launch_darkly/Makefile b/modules/launch_darkly/Makefile new file mode 100644 index 00000000000..6475b424a3e --- /dev/null +++ b/modules/launch_darkly/Makefile @@ -0,0 +1,12 @@ +# WARNING: do not run this directly, it should be run by the master Makefile + +include ../../Makefile.defs +auto_gen= +NAME=launch_darkly.so + + +DEFS+=-I$(LOCALBASE)/include +LIBS+=-L$(LOCALBASE)/lib -l ldserverapi -l curl -l pthread -l m -l pcre + +include ../../Makefile.modules + diff --git a/modules/launch_darkly/doc/contributors.xml b/modules/launch_darkly/doc/contributors.xml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/launch_darkly/doc/launch_darkly.xml b/modules/launch_darkly/doc/launch_darkly.xml new file mode 100644 index 00000000000..1603c68d525 --- /dev/null +++ b/modules/launch_darkly/doc/launch_darkly.xml @@ -0,0 +1,27 @@ + + + + + + +%docentities; + +]> + + + + launch_darkly Module + &osipsname; + + + + &admin; + &contrib; + + &docCopyrights; + ©right; 2023 Five9 Inc. + diff --git a/modules/launch_darkly/doc/launch_darkly_admin.xml b/modules/launch_darkly/doc/launch_darkly_admin.xml new file mode 100644 index 00000000000..6fe2a373351 --- /dev/null +++ b/modules/launch_darkly/doc/launch_darkly_admin.xml @@ -0,0 +1,235 @@ + + + + + &adminguide; + +
+ Overview + + This module implements support for the + Launch Darkly feature + management cloud. The module provide the conectivity to the cloud and + the ability to query for feature flags. + + + OpenSIPS uses the server side C/C++ SDK provided by Launch Darkly. + +
+ +
+ Dependencies +
+ &osips; Modules + + The following modules must be loaded before this module: + + + + none. + + + + +
+
+ External Libraries or Applications + + The following libraries or applications must be installed before + running &osips; with this module loaded: + + + + ldserverapi + + + + + + ldserverapi must be compiled and installed + from the official + GITHUB repository . + + + The instructions for a quick installations of the library (note that it has to be compiled as shared lib in order to be compatible with the OpenSIPS modules): + + +... + $ git clone https://github.com/launchdarkly/c-server-sdk.git + $ cd c-server-sdk + $ cmake -DBUILD_SHARED_LIBS=On -DBUILD_TESTING=OFF . + $ sudo make install +... + +
+
+ +
+ Exported Parameters + +
+ <varname>sdk_key</varname> (string) + + The LaunchDarkly SDK key used to connect to the service. This + is a mandatory parameter. + + + Set <varname>sdk_key</varname> parameter + +... +modparam("launch_darkly", "sdk_key", "sdk-12345678-abcd-12ab-1234-0123456789abc") +... + + +
+ +
+ <varname>ld_log_level</varname> (string) + + The LaunchDarkly specific log level to be used by the LD SDK/libray to + log its internal messages. Note that these log produced by the LD + library (according to this ld_log_level) will be further subject to + filtering according to the overall OpenSIPS log_level. + + + Accepted values are + LD_LOG_FATAL, + LD_LOG_CRITICAL, + LD_LOG_ERROR, + LD_LOG_WARNING, + LD_LOG_INFO, + LD_LOG_DEBUG, + LD_LOG_TRACE. + + + If not set or set to an unsupported value, the + LD_LOG_WARNING level will be used by default. + + + Set <varname>log_level</varname> parameter + +... +modparam("launch_darkly", "ld_log_level", "LD_LOG_CRITICAL") +... + + +
+ +
+ <varname>connect_wait</varname> (integer) + + The time to wait (in miliseconds) when connecting to the LD service. + An initial failure in connecting to the LD service may be addressed + by increasing this wait value. + + + The default value is 500 miliseconds. + + + Set <varname>connect_wait</varname> parameter + +... +modparam("launch_darkly", "connect_wait", 100) +... + + +
+ +
+ <varname>re_init_interval</varname> (integer) + + The minimum time interval (in seconds) to try again to init + the LD client in the situation when the module was not able to init + the LC connection at startup. In case of such failure, the module will + automatically re-try to init its LD client on-demand, whnever the + feature flag is checked from script, but not sooner than + `re_init_interval`. Note: if there are no flag checkings to be + performed, the re-init may be attempted longer than `re_init_interval`. + + + The default value is 10 seconds. + + + Set <varname>re_init_interval</varname> parameter + +... +modparam("launch_darkly", "re_init_interval", 30) +... + + +
+ + +
+ +
+ Exported Functions +
+ + <function moreinfo="none">ld_feature_enabled( flag, user, [user_extra], [fallback])</function> + + + Function to evaluate a LaunchDarkly boolean feature flag + + + Returns 1 if the flag was found TRUE + or -1 otherwise. + + + In case of error, the fallback (TRUE or FALSE) value will be + returned In such cases, a "fallback" TRUE is returned as 2 and a + fallback FALSE as -2, so you can may a difference between a real + TRUE (returned by the LD service) and a fallback TRUE due to an + error. + + + This function can be used from any route. + + + The function has the following parameters: + + + + + flag (string) - the key of the flag + to evaluate. May not be NULL or empty. + + + + + user (string) - the user to evaluate + the flag against. May not be NULL or empty. + + + + + user_extra (AVP, optional) - an AVP + holding one or multiple key-value attributes to be + attached to the user. The format of the AVP value is + "key=value". + + + + + fallback (int, optional) - the value + to be returned on error. By default FALSE will be returned. + + + + + + <function>ld_feature_enabled()</function> function usage + + ... + $avp(extra) = "domainId=123456"; + if (ld_feature_enabled("my-flag","opensips", $avp(extra), false)) + xlog("-------TRUE\n"); + else + xlog("-------FALSE\n"); + ... + + +
+
+ +
diff --git a/modules/launch_darkly/launch_darkly.c b/modules/launch_darkly/launch_darkly.c new file mode 100644 index 00000000000..a3459a9cdba --- /dev/null +++ b/modules/launch_darkly/launch_darkly.c @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2023 Five9 Inc. + * + * This file is part of opensips, a free SIP server. + * + * opensips 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 2 of the License, or + * (at your option) any later version. + * + * opensips 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + */ + +#include "../../sr_module.h" +#include "../../ut.h" +#include "../../mod_fix.h" +#include "ld_ops.h" + +static int mod_init(void); +static int child_init(int); + +static int fixup_check_avp(void** param); + +static int w_ld_feature_enabled(struct sip_msg *sip_msg, str *feat, str *user, + pv_spec_t *user_extra_avp, int* fallback); + +static char *ld_log_level_s = "LD_LOG_WARNING"; + + +static const param_export_t mod_params[] = { + {"sdk_key", STR_PARAM, &sdk_key}, + {"ld_log_level", STR_PARAM, &ld_log_level_s}, + {"connect_wait", INT_PARAM, &connect_wait}, + {"re_init_interval", INT_PARAM, &re_init_interval}, + {0,0,0} +}; + +static const cmd_export_t cmds[] = { + {"ld_feature_enabled",(cmd_function)w_ld_feature_enabled, { + {CMD_PARAM_STR, 0, 0}, + {CMD_PARAM_STR, 0, 0}, + {CMD_PARAM_VAR|CMD_PARAM_OPT, fixup_check_avp, 0}, + {CMD_PARAM_INT|CMD_PARAM_OPT, 0, 0}, + {0,0,0}}, + ALL_ROUTES}, + {0,0,{{0,0,0}},0} +}; + +struct module_exports exports = { + "launch_darkly", /* module name */ + MOD_TYPE_DEFAULT, /* class of this module */ + MODULE_VERSION, + DEFAULT_DLFLAGS, /* dlopen flags */ + NULL, /* load function */ + NULL, /* OpenSIPS module dependencies */ + cmds, /* exported functions */ + NULL, /* exported async functions */ + mod_params, /* exported parameters */ + NULL, /* exported statistics */ + NULL, /* exported MI functions */ + NULL, /* exported pseudo-variables */ + NULL, /* exported transformations */ + NULL, /* extra processes */ + NULL, /* module pre-initialization function */ + mod_init, /* module initialization function */ + NULL, /* response handling function */ + NULL, /* destroy function */ + child_init, /* per-child init function */ + NULL /* reload confirm function */ +}; + + +static int fixup_check_avp(void** param) +{ + if (((pv_spec_t *)*param)->type!=PVT_AVP) { + LM_ERR("the return parameter must be an AVP\n"); + return E_SCRIPT; + } + + return 0; +} + + +static int mod_init(void) +{ + if (sdk_key == NULL) { + LM_ERR("SDK key not configured via modparam!\n"); + return -1; + } + + set_ld_log_level(ld_log_level_s); + + return 0; +} + + +static int child_init(int rank) +{ + if (ld_init_child() < 0) { + LM_ERR("cannot init writing pipe\n"); + return -1; + } + + return 0; +} + +static int w_ld_feature_enabled(struct sip_msg *sip_msg, str *feat, str *user, + pv_spec_t *user_extra_avp, int *fallback) +{ + return ld_feature_enabled( feat, user, + user_extra_avp ? user_extra_avp->pvp.pvn.u.isname.name.n : -1, + fallback ? *fallback : -1 ); +} + diff --git a/modules/launch_darkly/ld_ops.c b/modules/launch_darkly/ld_ops.c new file mode 100644 index 00000000000..2f1b0fb5e38 --- /dev/null +++ b/modules/launch_darkly/ld_ops.c @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2023 Five9 Inc. + * + * This file is part of opensips, a free SIP server. + * + * opensips 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 2 of the License, or + * (at your option) any later version. + * + * opensips 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + */ + + +#include + +#include "../../pvar.h" +#include "../../ut.h" + +unsigned int connect_wait = 500; //milliseconds +unsigned int re_init_interval = 10; //seconds +char * sdk_key = NULL; + +static struct LDConfig *ld_cfg = NULL; +static struct LDClient *ld_client = NULL; +static unsigned int last_init_attempt_time = 0; +static int ld_log_level = LD_LOG_WARNING; + +void set_ld_log_level( char *log_level_s) +{ + if (strcasecmp( log_level_s, "LD_LOG_FATAL")==0) + ld_log_level = LD_LOG_FATAL; + else + if (strcasecmp( log_level_s, "LD_LOG_CRITICAL")==0) + ld_log_level = LD_LOG_CRITICAL; + else + if (strcasecmp( log_level_s, "LD_LOG_ERROR")==0) + ld_log_level = LD_LOG_ERROR; + else + if (strcasecmp( log_level_s, "LD_LOG_WARNING")==0) + ld_log_level = LD_LOG_WARNING; + else + if (strcasecmp( log_level_s, "LD_LOG_INFO")==0) + ld_log_level = LD_LOG_INFO; + else + if (strcasecmp( log_level_s, "LD_LOG_DEBUG")==0) + ld_log_level = LD_LOG_DEBUG; + else + if (strcasecmp( log_level_s, "LD_LOG_TRACE")==0) + ld_log_level = LD_LOG_TRACE; + else { + LM_WARN("unrecognized '%s' LG log level, using LD_LOG_WARNING\n", + log_level_s); + } +} + + +static void _oss_logger(const LDLogLevel level, const char *const text) +{ + /* + enum LDLogLevel { + LD_LOG_FATAL = 0, LD_LOG_CRITICAL, LD_LOG_ERROR, LD_LOG_WARNING, + LD_LOG_INFO, LD_LOG_DEBUG, LD_LOG_TRACE } + */ + int log_map[LD_LOG_TRACE+1] = {L_ALERT,L_CRIT,L_ERR,L_WARN, + L_INFO, L_DBG, L_DBG}; + + LM_GEN( log_map[level], "[LD] %s\n", text); + return; +} + + + +static int ld_client_init_attempt(void) +{ + /* maybe already connected? */ + if (ld_client) + return 0; + + /* too soon to retry a new connect ?*/ + if (last_init_attempt_time!=0 && + (last_init_attempt_time + re_init_interval > get_ticks()) ) + return -2; + + LM_DBG("attempting LD client re-init\n"); + + /* we do expect a valid ld config here */ + ld_client = LDClientInit( ld_cfg, connect_wait); + if (!LDClientIsInitialized(ld_client)) { + //LDClientClose(ld_client); this triggered a double free in LD lib :-/ + ld_client = NULL; + last_init_attempt_time = get_ticks(); + return -1; + } + last_init_attempt_time = 0; + + return 0; +} + + +int ld_init_child(void) +{ + LDConfigureGlobalLogger( ld_log_level, _oss_logger); + + LDGlobalInit(); + + LM_DBG("LD globally initialized, proceeding with the connect\n"); + + ld_cfg = LDConfigNew( sdk_key ); + if (ld_cfg==NULL) { + LM_ERR("failed to perform LD config\n"); + return -1; + } + + if (ld_client_init_attempt()!=0) + LM_ERR("LD client failed to initialize, proceeding offline\n"); + else + LM_DBG("LD client initialized\n"); + + return 0; +} + + +int ld_feature_enabled(str *feat, str *user, int user_extra_avp_id, + int fallback) +{ + struct LDUser *ld_user; + struct LDJSON *ld_extra, *ld_val; + struct LDDetails ld_details; + LDBoolean ld_res; + struct usr_avp *avp; + int_str val; + str s_nt, extra_key, extra_val; + char *p; + + if (ld_client==NULL && ld_client_init_attempt()<0) { + LM_ERR("not having a connected LD client :(\n"); + goto error; + } + + if (pkg_nt_str_dup( &s_nt, user)<0) { + LM_ERR("failed to pkg_nt duplicate the user\n"); + goto error; + } + ld_user = LDUserNew( s_nt.s ); + pkg_free(s_nt.s); + if (ld_user==NULL) { + return -1; + LM_ERR("failed to create new LD user\n"); + goto error; + } + + /* do we have custom key-val pairs to add to the user? */ + if (user_extra_avp_id>=0) { + avp = NULL; + ld_extra = NULL; + /* iterate all the AVPs with the keys */ + while ((avp=search_first_avp(AVP_VAL_STR,user_extra_avp_id,&val,avp))!=NULL) { + /* split and evaluate the value part */ + if ( (p=q_memchr( val.s.s, '=', val.s.len))==NULL) { + LM_ERR("extra <%.*s> has no key separtor '=', discarding\n", + val.s.len, val.s.s); + continue; + } + extra_key.s = val.s.s; + extra_key.len = p-val.s.s; + p++; + if (p==val.s.s+val.s.len) { + LM_ERR("extra <%.*s> has no value, discarding\n", + val.s.len, val.s.s); + continue; + } + extra_val.s = p; + extra_val.len = val.s.s+val.s.len-p; + + /* add the new extra to the user */ + if (ld_extra==NULL) { + ld_extra = LDNewObject(); + if (ld_extra==NULL) { + LM_ERR("failed to create new user object\n"); + goto error1; + } + } + + /* create the new value */ + if (pkg_nt_str_dup( &s_nt, &extra_val)<0) { + LM_ERR("failed to pkg_nt duplicate the extra value\n"); + goto error1; + } + ld_val = LDNewText( s_nt.s ); + pkg_free(s_nt.s); + if (ld_val==NULL) { + LM_ERR("failed create new extra LD val\n"); + goto error1; + } + + /* add the value as key */ + if (pkg_nt_str_dup( &s_nt, &extra_key)<0) { + LM_ERR("failed to pkg_nt duplicate the extra key\n"); + goto error1; + } + if (!LDObjectSetKey( ld_extra, s_nt.s, ld_val)) { + LM_ERR("failed to add new key+val to user extra\n"); + pkg_free(s_nt.s); + goto error1; + } + pkg_free(s_nt.s); + + } + + if (ld_extra) + LDUserSetCustom(ld_user, ld_extra); + } + + /* now, run the check */ + if (pkg_nt_str_dup( &s_nt, feat)<0) { + LM_ERR("failed to pkg_nt duplicate the feature name\n"); + goto error1; + } + ld_res = LDBoolVariation( ld_client, ld_user, s_nt.s, + fallback?LDBooleanTrue:LDBooleanFalse, &ld_details); + ld_res = ld_res ? 1 : -1; + + /* any error ? */ + if (ld_details.reason==LD_ERROR) { + ld_res = 2 * ld_res; //return some internal error indication + switch (ld_details.extra.errorKind) { + case LD_CLIENT_NOT_READY: + LM_BUG("LD client not initialized at this point!?!\n"); + break; + case LD_NULL_KEY: + LM_ERR("LD flag key is empty/NULL\n"); + break; + case LD_STORE_ERROR: + LM_ERR("LD internal exception with the flag store\n"); + break; + case LD_FLAG_NOT_FOUND: + LM_ERR("the caller provided a flag key that did not match any known flag\n"); + break; + case LD_USER_NOT_SPECIFIED: + LM_ERR("LD user is empty/NULL!\n"); + break; + case LD_CLIENT_NOT_SPECIFIED: + LM_BUG("LD client is NULL?!?!\n"); + break; + case LD_MALFORMED_FLAG: + LM_ERR("internal inconsistency in the flag data, a rule specified a nonexistent variation\n"); + break; + case LD_WRONG_TYPE: + LM_ERR("the result value was not of the requested type- expected LDBoolVariation\n"); + break; + case LD_OOM: + LM_ERR("LD clientran out of memory.\n"); + break; + default: + LM_ERR("unknown %d error reported by LDBoolVariation\n",ld_details.extra.errorKind); + break; + } + } + + LM_DBG("feature flag %s is %s\n", s_nt.s, (ld_res>0)?"TRUE":"FALSE"); + pkg_free(s_nt.s); + + LDUserFree(ld_user); + return ld_res; + +error1: + LDUserFree(ld_user); +error: + return fallback?2:-2; +} diff --git a/modules/launch_darkly/ld_ops.h b/modules/launch_darkly/ld_ops.h new file mode 100644 index 00000000000..e0c5fc0e421 --- /dev/null +++ b/modules/launch_darkly/ld_ops.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Five9 Inc. + * + * This file is part of opensips, a free SIP server. + * + * opensips 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 2 of the License, or + * (at your option) any later version. + * + * opensips 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + */ + + +#ifndef _LD_OPS_H_ +#define _LD_OPS_H_ + +#include "../../str.h" + +extern char* sdk_key; +extern unsigned int connect_wait; +extern unsigned int re_init_interval; + +void set_ld_log_level( char *log_level_s); + +int ld_init_child(void); + +int ld_feature_enabled(str *feat, str *user, int user_extra_avp_id, + int fallback); + +#endif