From 5ffd3bb413beeff73a3bcce77bb098731c88d644 Mon Sep 17 00:00:00 2001 From: Bogdan-Andrei Iancu Date: Wed, 31 Jan 2024 13:39:54 +0200 Subject: [PATCH] Add new launch_darkly module 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. The development of this module was sponsored by Five9 https://www.five9.com/ --- Makefile.conf.template | 3 +- modules/launch_darkly/Makefile | 12 + modules/launch_darkly/doc/contributors.xml | 0 modules/launch_darkly/doc/launch_darkly.xml | 27 ++ .../launch_darkly/doc/launch_darkly_admin.xml | 235 +++++++++++++++ modules/launch_darkly/launch_darkly.c | 123 ++++++++ modules/launch_darkly/ld_ops.c | 280 ++++++++++++++++++ modules/launch_darkly/ld_ops.h | 40 +++ 8 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 modules/launch_darkly/Makefile create mode 100644 modules/launch_darkly/doc/contributors.xml create mode 100644 modules/launch_darkly/doc/launch_darkly.xml create mode 100644 modules/launch_darkly/doc/launch_darkly_admin.xml create mode 100644 modules/launch_darkly/launch_darkly.c create mode 100644 modules/launch_darkly/ld_ops.c create mode 100644 modules/launch_darkly/ld_ops.h 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