Documentation |
Documentation -> Manuals -> OpenSIPS 1.11 Development ManualThis page has been visited 13363 times. Pages for other versions: devel 3.4 3.3 3.2 3.1 Older versions: 1.11
Table of Content (hide)
1. IntroductionThe focus of the following document will be on the general architecture of OpenSIPS, as well as presenting all the major components and APIs that OpenSIPS exposes for building new modules / features.
2. General ArchitectureTBD \\ TBD 3. Memory ManagementOpenSIPS has it's own memory allocator, and this provides some important advantages over the system memory allocator :
3.1 Private (PKG) MemoryPrivate memory is only specific to a single OpenSIPS process. Since it has no visibility outside the current process, no locking mechanisms are required while using such memory.
Note the common use case where , before forking OpenSIPS processes, the developer stores some static variables in private memory of the main process. After forking, all the child processes will inherit that private memory chunk and each will have it's individual copy of it.
/* Parameters : size - size in bytes of the request private memory Returns : the actual allocated buffer, or NULL is case of error */ void *pkg_malloc(unsigned int size); /* Parameters : buf - the buffer to be freed */ void pkg_free(void *buf) /* Parameters : buf - buffer that we want to reallocate size - the new desired buffer size Returns : the new buffer address if reallocation is successful, or NULL in case of error. Note that pkg_realloc(NULL,size) is equivalent to pkg_malloc(size) */ void *pkg_realloc(void *buf, unsigned int size); 3.2 Shared (SHM) MemoryShared memory can be accessible from all OpenSIPS processes. Thus, generally speaking, all write access to a shared memory buffer should be guarded by some form of synchronization mechanism in order to ensure consistency. mem/shm_mem.h exposes all the shared memory related functions : /* Parameters : size - size in bytes of the request shared memory Returns : the actual allocated buffer, or NULL is case of error */ void *shm_malloc(unsigned int size); /* Parameters : buf - the buffer to be freed */ void shm_free(void *buf) /* Parameters : buf - buffer that we want to reallocate size - the new desired buffer size Returns : the new buffer address if reallocation is successful, or NULL in case of error. Note that shm_realloc(NULL,size) is equivalent to shm_malloc(size) */ void *shm_realloc(void *buf, unsigned int size); 4. Parsing SIP MessagesThe OpenSIPS SIP message parser is a lazy parser, which performs very well in terms of performance. The behavior is the following :
4.1 Generic Header ParserThe generic parser of SIP headers is exposed by parser/msg_parser.h . The function to be used is : /* Parameters : msg : the SIP message that needs to be parsed – see parser/msg_parser.h for details on the struct sip_msg structure flags : bitmask of header types that need to be parsed next : specifies whether the parser should explicitly force the parsing of new headers from the provided bitmask, even though those header types were already previously found. Can be useful when trying to find a second occurrence of a header ( in the case that header can appear multiple times in a SIP message – eg. Route ) Returns : 0 in case of error, -1 in case of error ( either header was not found or some other error occurred ). */ int parse_headers(struct sip_msg* msg, hdr_flags_t flags, int next);
if(parse_headers(req,HDR_CALLID_F|HDR_TO_F|HDR_FROM_F,0)<0 || !req->callid || !req->to || !req->from) { LM_ERR("bad request or missing CALLID/TO/FROM hdr \n"); return -1; } HDR_EOH_F is exposed in case the developer wants to parse all the headers in the current SIP message. The parse_headers() function will not duplicate SIP headers at all – the hooks in the struct sip_msg structure will be populated with pointers that point directly in the SIP message buffer.
LM_INFO(“The callid header name is %.*s and the callid header body is %.*s \n”, req->callid->name.len, req->callid->name.s, req->callid->bodylen. req->callid->body.s); 4.2 Specific Header ParsingFor parsing a specific header type, and extracting the header type relevant information, the parser/ folder contains all implementations for known headers.
The naming convention is that parser/parse_X.h will expose the parsing for the X header name.
int parse_to_header( struct sip_msg *msg) { struct to_body* to_b; if ( !msg->to && ( parse_headers(msg,HDR_TO_F,0)==-1 || !msg->to)) { LM_ERR("bad msg or missing To header\n"); goto error; } /* maybe the header is already parsed! */ if (msg->to->parsed) return 0; /* bad luck! :-( - we have to parse it */ /* first, get some memory */ to_b = pkg_malloc(sizeof(struct to_body)); if (to_b == 0) { LM_ERR("out of pkg_memory\n"); goto error; } /* now parse it!! */ memset(to_b, 0, sizeof(struct to_body)); parse_to(msg->to->body.s,msg->to->body.s+msg->to->body.len+1,to_b); if (to_b->error == PARSE_ERROR) { LM_ERR("bad to header\n"); pkg_free(to_b); goto error; } msg->to->parsed = to_b; return 0; error: return -1; }
Note that the void *parsed element in the hdr_field structure will contain the header specific parser structure, which will also be allocated into private memory and automatically freed when the SIP message processing has finished.
LM_INFO(“The TO header tag value is %.*s \n”,get_to(msg)->tag_value.len, get_to(msg)->tag_value.s); 4.3 Parsing SIP URIsThe OpenSIPS parser also exposes the functionality of parsing individual SIP URI. /* Parameters : buf - the string which contains our SIP URI len - length of the SIP URI buffer uri - structure which will be populated by the function in case of success. See full struct sip_uri members in parser/msg_parser.h Returns : 0 in case of success, negative value in case of error parsing the URI */ int parse_uri(char *buf, int len, struct sip_uri* uri);
/* make sure TO header is parsed before this */ struct to_body *tb = get_to(msg); if (parse_uri(tb->uri.s, tb->uri.len , &tb->parsed_uri)<0) { LM_ERR("failed to parse To uri\n"); return -1; } LM_INFO(“TO URI user is %.*s and TO URI domain is %.*s\n”, tb->parsed_uri.user.len, tb->parsed_uri.user.s, tb->parsed_uri.domain.len, tb->parsed_uri.domain.s); 4.4 Parsing the SDP BodyOpenSIPS exposes functions for operating on the SIP message body.
/* Parameters : msg - the SIP message to fetch the body for body - output param, which will hold the body pointer inside the SIP message and the body length, or {NULL,0} in case of no body present Returns : 0 in case of success, or -1 in the case of parsing errors ( the function needs to internally parse all the headers in order to detect the body length ). */ int get_body(struct sip_msg *msg, str *body)
/* Parameters : _m - the SIP message to have it's SDP parsed Returns : 0 in case of success, negative in case of error */ int parse_sdp(struct sip_msg* _m);
5. Changing SIP MessagesThe standard mechanism for performing changes on SIP messages within OpenSIPS is by using the so called lumps system.
5.1 SIP Message LumpsThis type of lumps operate on the current SIP message context.
Delete Lumpsdata_lump.h exposes /* Parameters : msg - the SIP message the lump will affect offset - the offset in the SIP message at which to start deleting len - the number of characters to delete from the SIP message type - indication on which header the current lump affects ( can be 0 ) Returns : the created lump structure for deleting part of the SIP message. Can be further used to chain together different types of lumps in the message attached list of lumps. NULL is returned in case of internal error. */ struct lump* del_lump(struct sip_msg* msg, unsigned int offset, unsigned int len, enum _hdr_types_t type);
/* first parse the header to figure out where it actually starts in the SIP message */ if( parse_headers(msg,HDR_RPID_F,0)<0 || msg->rpid == NULL ){ LM_DBG(“No rpid header – nothing to delete \n”); return 0; } /* delete the entire RPID header */ if ( del_lump(msg, msg->rpid->name.s-msg->buf, msg->rpid->len,HDR_RPID_T )== NULL) { LM_ERR(“Failed to delete RPID header \n”); return -1; } Add Lumpsdata_lump.h exposes /* Parameters : after/before - the lump where we will connect our new lump new_hdr - string to be added len - length of the string to be added type - header type that is affected by the current change ( can be 0 ) Returns : the created lump structure for adding to the SIP message. Can be further used to chain together different types of lumps in the message attached list of lumps. NULL is returned in case of internal error. */ struct lump* insert_new_lump_after(struct lump* after, char* new_hdr, unsigned int len, enum _hdr_types_t type); struct lump* insert_new_lump_before(struct lump* before, char* new_hdr, unsigned int len,enum _hdr_types_t type);
/* Parameters : msg - the SIP message that will be affected by the lump anchor offset - the offset in the SIP message where the anchor will be placed len - not currently used ( should be 0 ) type - header type that is affected by the current change ( can be 0 ) Returns: the created lump structure for adding to the SIP message. Can be further used to chain together different types of lumps in the message attached list of lumps. NULL is returned in case of internal error. */ struct lump* anchor_lump(struct sip_msg* msg, unsigned int offset, int unsigned len, enum _hdr_types_t type)
/* make sure we detect all headers */ if (parse_headers(msg, HDR_EOH_F, 0) == -1) { LM_ERR("error while parsing message\n"); return -1; } /* add the anchor at the very end of the SIP headers */ anchor = anchor_lump(msg, msg->unparsed - msg->buf, 0, 0); if (anchor == NULL) { LM_ERR(“Failed to create lump anchor\n”); return -1; } len = sizeof(“MY_HDR: MY_VAL\r\n”) -1; new_hdr=pkg_malloc(len); if (!new_hdr) { LM_ERR(“No more pkg mem\n”); return -1; } memcpy(new_hdr,”MY_HDR: MY_VAL\r\n”,len); if (insert_new_lump_after(anchor, new_hdr, len, 0) == 0) { LM_ERR("can't insert lump\n"); pkg_free(new_hdr); return -1; } /* job done, the PKG new_hdr mem will be free internally when the lump will be applied */ return 0; If we want to replace a particular part of a SIP message, the operation can be split in two steps, first deleting the part we don't need anymore by calling del_lump, and then using the returned lump to add a new lump after it.
/* first parse the header to figure out where it actually starts in the SIP message */ if( parse_headers(msg,HDR_RPID_F,0)<0 || msg->rpid == NULL ){ LM_DBG(“No rpid header – nothing to delete \n”); return 0; } /* delete just the contents of the RPID header */ del = del_lump(msg, msg->rpid->body.s-msg->buf, msg->rpid->body.len,HDR_RPID_T); if ( del == NULL) { LM_ERR(“Failed to delete RPID header \n”); return -1; } len = sizeof(“sip:new_rpid@my_domain.com\r\n”) -1; new_rpid=pkg_malloc(len); if (!new_rpid) { LM_ERR(“No more pkg mem\n”); return -1; } memcpy(new_rpid,“sip:new_rpid@my_domain.com\r\n”,len); if(insert_new_lump_after(del,new_rpid,len,HDR_RPID_T)==NULL) { LM_ERR("Failed to insert new callid\n"); pkg_free(new_rpid); return -1; } 5.2 SIP Reply LumpsWhen used in the case of a SIP request, these lumps will operate on the SIP reply that will be internally generated when rejecting a request from within OpenSIPS ( if the Request if forwarded instead of rejected at OpenSIPS level, these lumps will have no effect ). Since the reply will be internally generated by OpenSIPS, the Reply Lumps can only add new content.
/* Parameters : msg - the SIP Request that the reply will be generated for s - the string to be added to the reply len - the length of the string to be added flags - Since the reply will be generated by OpenSIPS, it is important to mark your lump if it should be added to the Reply headers or to the Reply body. Relevant flags for these cases are LUMP_RPL_HDR and LUMP_RPL_BODY. Returns : the created lump structure for adding to the SIP reply. Can be further used to chain together lumps in the message attached list of lumps. NULL is returned in case of internal error. */ struct lump_rpl* add_lump_rpl(struct sip_msg *msg, char *s, int len, int flags);
static char ct[CT_LEN] = “Contact: opensips@my_domain.com\r\n”; /* we are adding a lump to the headers, so we pass the LUMP_RPL_HDR flag also , our buffer is located in a static buffer, thus no need for the core to allocate memory for this lump, we also pass the LUMP_RPL_NODUP flag */ if (add_lump_rpl(msg, ct, CT_LEN, LUMP_RPL_HDR | LUMP_RPL_NODUP)==0) { LM_ERR("unable to add lump\n"); return -1; } 6. Extending OpenSIPS core Config FileOpenSIPS uses flex and bison in order to parse the configuration file and then build the entire action tree that a SIP message will go through once it is read from network level. 6.1 Adding a core parameterIn the following step by step tutorial, we will follow the implementation of the children core parameter, which is an integer controlling the number of OpenSIPS processes per UDP interface. extern int children_no;
/* Default value in case the parameter is not set from the script */ int children_no = 8;
%token CHILDREN
| CHILDREN EQUAL NUMBER { children_no=$3; } | CHILDREN EQUAL error { yyerror("number expected"); } (:sourceend:)
6.2 Adding a core functionIn the following step by step tutorial, we will follow the implementation of the xlog core parameter, which is used to print debugging information to the logging facility. Note that xlog can receive either a single parameter ( the string to be printed ), or two parameters ( the log level and then the string to be printed ). First, the grammar will have to be extended. In cfg,y we have : %token XLOG ... ... | XLOG LPAREN STRING RPAREN { mk_action1($$, XLOG_T, STR_ST, $3); } XLOG LPAREN STRING COMMA STRING RPAREN { mk_action2($$, XLOG_T, STR_ST, STR_ST, $3, $5); }
case XLOG_T: s.s = (char*)t->elem[1].u.data; if (s.s == NULL) { /* commands have only one parameter */ s.s = (char *)t->elem[0].u.data; s.len = strlen(s.s); if(s.len==0) { LM_ERR("param is empty string!\n"); return E_CFG; } /* parse the format provided to xlog - we can have variables inside */ if(pv_parse_format(&s ,&model) || model==NULL) { LM_ERR("wrong format [%s] for value param!\n", s.s); ret=E_BUG; goto error; } /* overwrite the data that will be passed to contain our new parsed model */ t->elem[0].u.data = (void*)model; t->elem[0].type = SCRIPTVAR_ELEM_ST; } else { /* two parameters */
case XLOG_T: /* add helpers for tracing the script */ script_trace("core", "xlog", msg, a->line) ; if (a->elem[1].u.data != NULL) { /* we have two parameters */ /* do security checks for the types of the provided parameters, second param has to be a SCRIPTVAR model as we've coded in the fixup */ if (a->elem[1].type != SCRIPTVAR_ELEM_ST) { LM_ALERT("BUG in xlog() type %d\n", a->elem[1].type); ret=E_BUG; break; } /* log level should be a plaintext string */ if (a->elem[0].type != STR_ST) { LM_ALERT("BUG in xlog() type %d\n", a->elem[0].type); ret=E_BUG; break; } /* call our C code function implementing the desired actions */ if (xlog_2(msg,a->elem[0].u.data, a->elem[1].u.data) < 0) { LM_ALERT("Cannot print xlog debug message"); break; } } else { /* one parameter case */ } 6.3 Adding a core Pseudo-VariableAll the OpenSIPS core pseudo-variables are defined in pvar.c : static pv_export_t _pv_names_table[] = { {{"avp", (sizeof("avp")-1)}, PVT_AVP, pv_get_avp, pv_set_avp, pv_parse_avp_name, pv_parse_index, 0, 0}, {{"hdr", (sizeof("hdr")-1)}, PVT_HDR, pv_get_hdr, 0, pv_parse_hdr_name, pv_parse_index, 0, 0}, {{"hdrcnt", (sizeof("hdrcnt")-1)}, PVT_HDRCNT, pv_get_hdrcnt, 0, pv_parse_hdr_name, 0, 0, 0}, {{"var", (sizeof("var")-1)}, PVT_SCRIPTVAR, pv_get_scriptvar, pv_set_scriptvar, pv_parse_scriptvar_name, 0, 0, 0}, {{"ai", (sizeof("ai")-1)}, /* */ PVT_PAI_URI, pv_get_pai, 0, 0, 0, 0, 0}, {{"au", (sizeof("au")-1)}, /* */ PVT_AUTH_USERNAME, pv_get_authattr, 0, 0, 0, pv_init_iname, 1}, ... ... ...
/*! \brief * PV spec format: * - $class_name * - $class_name(inner_name) * - $(class_name[index]) * - $(class_name(inner_name)[index]) * - $(class_name{transformation}) * - $(class_name(inner_name){transformation}) * - $(class_name[index]{transformation}) * - $(class_name(inner_name)[index]{transformation}) */ typedef struct _pv_export { str name; /*!< class name of PV */ pv_type_t type; /*!< type of PV */ pv_getf_t getf; /*!< function to get the value */ pv_setf_t setf; /*!< function to set the value */ pv_parse_name_f parse_name; /*!< function to parse the inner name */ pv_parse_index_f parse_index; /*!< function to parse the index of PV */ pv_init_param_f init_param; /*!< function to init the PV spec */ int iparam; /*!< parameter for the init function */ } pv_export_t;
{{"ru", (sizeof("ru")-1)}, /* */ PVT_RURI, pv_get_ruri, pv_set_ruri, 0, 0, 0, 0}, Our new pvar will be accessible from script by using $ru. Read access from the script will lead to pv_get_ruri getting called, while write requests to $ru will make a call to pv_set_ruri.
/* Parameters : msg - the message context to evaluate the current pvar param - the parameter provided for evaluating the pvar res - the output value of our pvar Returns : 0 in case of success, negative in case of error */ static int pv_get_ruri(struct sip_msg *msg, pv_param_t *param, pv_value_t *res) { if(msg==NULL || res==NULL) return -1; if(msg->first_line.type == SIP_REPLY) /* REPLY doesnt have a ruri */ return pv_get_null(msg, param, res); if(msg->parsed_uri_ok==0 /* R-URI not parsed*/ && parse_sip_msg_uri(msg)<0) { LM_ERR("failed to parse the R-URI\n"); return pv_get_null(msg, param, res); } if (msg->new_uri.s!=NULL) return pv_get_strval(msg, param, res, &msg->new_uri); return pv_get_strval(msg, param, res, &msg->first_line.u.request.uri); }
For all read access on the PVARs from contexts where the PVAR does not have any meaningful value (eg. Request-URI from a Reply Context), make sure to use pv_get_null to signal this to the script writer.
/* Parameters : msg - the SIP message to apply the changes to param - the parameter provided for evaluating the pvar op - further indication on the type of write access to be done val - value to be pushed to our pvar Returns : 0 in case of success, negative in case of error */ int pv_set_ruri(struct sip_msg* msg, pv_param_t *param, int op, pv_value_t *val) { if(msg==NULL || param==NULL || val==NULL) { LM_ERR("bad parameters\n"); return -1; } /* type checking, we can only push strings to R-URI */ if(!(val->flags&PV_VAL_STR)) { LM_ERR("str value required to set R-URI\n"); goto error; } /* populate the message R-URI with the string value from the provided val */ if (set_ruri( msg, &val->rs)!=0) { LM_ERR("failed to set RURI\n"); goto error; } return 0; error: return -1; } 7. Adding TransformationsThe so called transformations are methods operating directly on the OpenSIPS various pseudo-variables. A transformation takes as input the value of the provided pseudo-variable and processes it, outputing a 'transformed' version.
# example of usage $var(tutorial) = “OpenSIPSDevel”; xlog(“Our variable has $(var(tutorial){s.len}) characters \n”); Note that transformations can be chained together, which will have an impact on the transformations C development
$var(our_uri) = “sip:vlad@opensips.org”; xlog(“The username of our URI has $(var(our_uri){uri.user}{s.len}) characters \n”); In our examples, uri and s are the so called classes of transformations, while user and len are the actual operations within the class. We will further follow the implementation of the uri class of transformation, and then focusing on the user function. Adding new classes of transformations is done in transformations.h , where , for our example's case, TR_URI was added. Then, each class of transformations should have it's own functions enumeration, which is our case is enum _tr_uri_subtype . transformations.c holds the actual implementation of transformations. parse_transformation will get called when the script needs to evaluate a transformation. There, we should add the matching for our new class of transformations : else if(tclass.len==3 && strncasecmp(tclass.s, "uri", 3)==0) { t->type = TR_URI; t->trf = tr_eval_uri; s.s = p; s.len = in->s + in->len - p; p0 = tr_parse_uri(&s, t); if(p0==NULL) goto error; p = p0; }
p = in->s; name.s = in->s; /* find next token */ while(*p && *p!=TR_PARAM_MARKER && *p!=TR_RBRACKET) p++; if(*p=='\0') { LM_ERR("invalid transformation: %.*s\n", in->len, in->s); goto error; } name.len = p - name.s; trim(&name); if(name.len==4 && strncasecmp(name.s, "user", 4)==0) { t->subtype = TR_URI_USER; return p; }
/* make a PKG copy of the input */ _tr_uri.s = (char*)pkg_malloc((val->rs.len+1)*sizeof(char)); memcpy(_tr_uri.s, val->rs.s, val->rs.len); _tr_uri.s[_tr_uri.len] = '\0'; ... ... /* parse the input URI */ if(parse_uri(_tr_uri.s, _tr_uri.len, &_tr_parsed_uri)!=0) { LM_ERR("invalid uri [%.*s]\n", val->rs.len,val->rs.s); return -1; } ... ... /* zero out the output val */ memset(val, 0, sizeof(pv_value_t)); /* the output pvar will be a string */ val->flags = PV_VAL_STR; switch(subtype) { case TR_URI_USER: val->rs = (_tr_parsed_uri.user.s)?_tr_parsed_uri.user:_tr_empty; break; } 8. Locking APIOpenSIPS has it's own locking API, and it is recommended to use it instead of the system exposed locks, since they offer greater flexibility - depending on the usage case and the menuconfig provided compilation flags, the OpenSIPS generic locks can be converted either to busy locks, futexes, SysV locks, etc.
8.1 Single Lock APIThe API can be used by including “locking.h” . The OpenSIPS generic lock is defined by the gen_lock_t structure. /* Returns : A shared memory allocated lock, or NULL in case of an error. */ gen_lock_t *lock_alloc(void);
/* Parameters : lock - the lock instance to be initialized Returns : The initialized lock in case of success, or NULL in case of error. */ gen_lock_t* lock_init(gen_lock_t* lock);
/* Parameters : lock - the lock to be acquired */ void lock_get(gen_lock_t *lock);
/* Parameters : lock - the lock to be released. */ void lock_release(gen_lock_t *lock);
/* Parameters : lock - the lock to be destroyed */ void lock_destroy(gen_lock_t *lock); /* Parameters : lock - the lock to be deallocated */ void lock_dealloc(gen_lock_t *lock);
gen_lock_t *my_lock; int init_function(void) { /* … */ my_lock = lock_alloc(); if (my_lock == NULL) { LM_ERR(“Failed to allocate lock \n”); return -1; } if (lock_init(my_lock) == NULL) { LM_ERR(“Failed to init lock \n”); return -1; } /* … */ return 0; } int do_work(void) { /* … */ lock_get(my_lock) /* critical region protected by our lock generally recommended to keep critical regions short I/O operations to be avoided in such critical regions */ lock_release(my_lock) /* … */ } void destroy_function(void) { /* … */ lock_destroy(my_lock); lock_dealloc(my_lock); /* … */ } 8.2 Lock Set APIOperating on an entire array of locks can become very useful when dealing with structures like hashes, where you would need a lock per each hash entry. /* Returns : A shared memory allocated lock set, or NULL in case of an error. */ gen_lock_set_t *lock_set_alloc(void);
/* Parameters : lock - the lock set instance to be initialized Returns : The initialized lock in case of success, or NULL in case of error. */ gen_lock_set_t* lock_set_init(gen_lock_set_t* lock);
/* Parameters : lock - the lock to be acquired entry - the entry in the lock set that needs to be acquired */ void lock_set_get(gen_lock_set_t *lock,int entry);
/* Parameters : lock - the lock to be released. entry - the entry in the lock set that needs to be released */ void lock_set_release(gen_lock_set_t *lock,int entry);
/* Parameters : lock - the lock set to be destroyed */ void lock_set_destroy(gen_lock_set_t *lock); /* Parameters : lock - the lock set to be deallocated */ void lock_set_dealloc(gen_lock_set_t *lock);
gen_lock_set_t *my_lock; int init_function(void) { /* … */ /* allocate lock set with 32 entries */ my_lock = lock_set_alloc(32); if (my_lock == NULL) { LM_ERR(“Failed to allocate lock set \n”); return -1; } if (lock_set_init(my_lock) == NULL) { LM_ERR(“Failed to init lock set \n”); return -1; } /* … */ return 0; } int do_work(void) { /* … */ /* acquire entry 5 in the lock set */ lock_set_get(my_lock,5) /* also acquire entry 21 in the lock set */ lock_set_get(my_lock,21); /* critical region protected by our lock generally recommended to keep critical regions short I/O operations to be avoided in such critical regions */ lock_set_release(my_lock,21); lock_set_release(my_lock,5); /* … */ } void destroy_function(void) { /* … */ lock_set_destroy(my_lock); lock_set_dealloc(my_lock); /* … */ } 8.3 Readers-Writers Locking APIA readers-writer lock is like a mutex, in that it controls access to a shared resource, allowing concurrent access to multiple threads for reading but restricting access to a single thread for writes (or other changes) to the resource. Allocating a new readers-writers lock into shared memory and initializing it is done by calling lock_init_rw : /* Returns : A shared memory allocated rw lock, or NULL in case of an error. */ inline static rw_lock_t * lock_init_rw(void);
/* Parameters : lock - the lock to be acquired */ void lock_start_read(rw_lock_t * lock);
/* Parameters : lock - the lock to be released */ void lock_stop_read(rw_lock_t * lock);
/* Parameters : lock - the lock to be acquired */ void lock_start_write(rw_lock_t * lock);
/* Parameters : lock - the lock to be release */ void lock_stop_write(rw_lock_t * lock);
/* Parameters : lock - the lock to be destroyed */ void lock_destroy_rw(rw_lock_t * lock); 9. Timer APIOpenSIPS exposes it's own API for implementing timer functions, with seconds and microsecond precision. 9.1 Global Timer Processtimer.h exposes all the relevant functionalities for operating the OpenSIPS timers. For registering a new timer function with second precision, use : /* Parameters : label – opaque string containing a short timer function description ( to be used for logging ) f – the actual function to be called param – parameter to be provided to the timer function interval – the interval, in seconds, that the function needs to be called at Returns : 0 in case of success, negative code in case of internal error. */ int register_timer(char *label, timer_function f, void* param, unsigned int interval); /* The seconds callback Parameters : ticks - represents the current number of seconds since OpenSIPS startup when the callback is called at param - is the parameter provided at timer function registration. */ typedef void (timer_function)(unsigned int ticks, void* param);
/* Parameters : label – opaque string containing a short timer function description ( to be used for logging ) f – the actual function to be called param – parameter to be provided to the timer function interval – the interval, in microseconds, that the function needs to be called at Returns : 0 in case of success, negative code in case of internal error. */ int register_utimer(char *label, utimer_function f, void* param, unsigned int interval); 9.2 Dedicated Timer ProcessSince, by default, all the registered timer functions are called from within the same process context, in case you are writing a timer process that is doing I/O, it is better to register an entirely new process where to run your code, since your function might slow down all the other timer functions running in OpenSIPS. /* Parameters : label - opaque string containing a short timer function description ( to be used for logging ) f - the actual function to be called param - parameter to be provided to the timer function interval - the interval, in seconds, that the function needs to be called at flags - flags controlling process behavior. Currently only option is TIMER_PROC_INIT_FLAG , which leads to child_init to be called in the new timer process context. To be used when inside the timer you need to operate various I/O options which generally require a per process connection. Returns : struct sr_timer_process pointer in case of success, or NULL in case of error. */ void* register_timer_process(char *label, timer_function f, void* param, unsigned int interval, unsigned int flags);
/* Parameters: label – opaque string containing a short timer function description ( to be used for logging ) f – the actual function to be called param – parameter to be provided to the timer function interval – the interval, in seconds, that the function needs to be called at timer – the struct sr_timer_process pointer obtained by previously calling register_timer_process Returns: 0 in case of success, negative code in case of internal error. */ int append_timer_to_process( char *label, timer_function f, void* param, unsigned int interval, void *timer);
Important to note here that all the above timer related functions MUST be called in the context of the attendant process before forking is done ( so either from the modules mod_init or directly from the core, before forking ). Below is a code snippet exemplifying how the dialog module's code used for registering two timers, with an option to either use the global timer process or to have it's own separate timer : if (dlg_have_own_timer_proc) { LM_INFO("Running with dedicated dialog timer process\n"); dlg_own_timer_proc = register_timer_process( "dlg-timer", dlg_timer_routine, NULL,1,TIMER_PROC_INIT_FLAG ); if (dlg_own_timer_proc == NULL) { LM_ERR("Failed to init dialog own timer proc\n"); return -1; } if (append_timer_to_process("dlg-pinger", dlg_ping_routine, NULL, ping_interval,dlg_own_timer_proc) < 0) { LM_ERR("Failed to append ping timer \n"); return -1; } } else { if ( register_timer( "dlg-timer", dlg_timer_routine, NULL, 1)<0 ) { LM_ERR("failed to register timer \n"); return -1; } if ( register_timer( "dlg-pinger", dlg_ping_routine, NULL, ping_interval)<0) { LM_ERR("failed to register timer 2 \n"); return -1; } }
/* Returns : the number of seconds elapsed from OpenSIPS start */ unsigned int get_ticks(void); /* Returns: the number of microseconds elapsed from OpenSIPS start */ utime_t get_uticks(void); 10. Management Interface APIThe Management Interface is the abstract layer that is commonly used to control and monitor OpenSIPS. The MI Interface supports multiple actual back-ends ( eg. FIFO, Datagram, XMLRPC, HTTP GET JSON, etc ) - due to the modularity of the interface and also due to the clear separation between the logic and the transport layer, the developer just defines the functions to be externally called, and then it is up to the OpenSIPS script writer to chose what transport he will actually use for controlling OpenSIPS.
typedef struct mi_export_ { /* the name of the function ( users will call this from their transport of choice */ char *name; /* short description of the usage of this function */ char *help; /* actual function that will get called */ mi_cmd_f *cmd; /* flags for this function. Currently options are : - MI_ASYNC_RPL_FLAG - the function has an asynchronous behaviour ( eg: MI functions that send SIP messages and wait for their reply ) - MI_NO_INPUT_FLAG - the function does not receive any parameters */ unsigned int flags; /* parameter that will be passed when the cmd function gets called */ void *param; /* the initialization function to be called by OpenSIPS ( one time ) */ mi_child_init_f *init_f; }mi_export_t; /* Example of core MI exported function */ static mi_export_t mi_core_cmds[] = { { "uptime", "prints various time information about OpenSIPS - " "when it started to run, for how long it runs", mi_uptime, MI_NO_INPUT_FLAG, 0, init_mi_uptime }, { "version", "prints the version string of a runningOpenSIPS", mi_version, MI_NO_INPUT_FLAG, 0, 0 }, { "pwd", "prints the working directory of OpenSIPS", mi_pwd, MI_NO_INPUT_FLAG, 0, 0 }, ... ... ... /* For exporting the populated array of MI functions Parameters : mod_name : the name of the module exporting these functions mis : the array of exported MI functions Returns : 0 on success, negative in case of error */ int register_mi_mod( char *mod_name, mi_export_t *mis); /* Example of usage */ if (register_mi_mod( "core", mi_core_cmds)<0) { LM_ERR("unable to register core MI cmds\n"); return -1; }
/* Parameters : input : the tree that contains the command paramenters param : the parameter provided at function registration Returns : A mi_root tree containing the function reply */ typedef struct mi_root* (mi_cmd_f)(struct mi_root *input, void *param); /* below are the used structures for representing the tree root and the tree nodes */ struct mi_root { /* int code - similar to SIP or HTTP code */ unsigned int code; /* string reason for code - similar to SIP or HTTP reason */ str reason; /* handler in case of asynchronous MI commands */ struct mi_handler *async_hdl; /* the actual root node in our tree */ struct mi_node node; }; struct mi_node { str value; str name; unsigned int flags; struct mi_node *kids; struct mi_node *next; struct mi_node *last; struct mi_attr *attributes; }; struct mi_attr{ str name; str value; struct mi_attr *next; };
/* Use for creating a new output reply tree Parameters : code : success code for this tree ( >=200<300 for success, anything else for errors ) reason : string reasons representation for the code reason_len : length of the reason parameter Returns : A new mi_root tree, or NULL in case of error. Note that this function will allocate the node in PKG and it typically has to be returned - the freeing will be done in the MI core, after the output tree is written by the transport module */ struct mi_root *init_mi_tree(unsigned int code, char *reason, int reason_len); /* Adding a new child node to our tree - typically first called to mi_root->node.kids Parameters : parent : the parent node for our newly added node flags : Current options are : MI_DUP_NAME : the name of this node needs to be duplicated in PKG MI_DUP_VALUE : the value of the current node needs to be duplicated in PKG name : the name of the current node name_len : length of the node's name value : the value of the current node value_len : length of the node's value */ struct mi_node *add_mi_node_child(struct mi_node *parent, int flags, char *name, int name_len, char *value, int value_len); /* Adding a new sibling node to one of our nodes Parameters : brother : the brother node for our newly added node flags : Current options are : MI_DUP_NAME : the name of this node needs to be duplicated in PKG MI_DUP_VALUE : the value of the current node needs to be duplicated in PKG name : the name of the current node name_len : length of the node's name value : the value of the current node value_len : length of the node's value */ struct mi_node *add_mi_node_sibling(struct mi_node *brother, int flags, char *name, int name_len, char *value, int value_len); /* Adding a new attribute to one of our nodes node : the node we will be adding the key-value attribute to flags : Current options are : MI_DUP_NAME : the name of this attribute needs to be duplicated in PKG MI_DUP_VALUE : the value of the current attribute needs to be duplicated in PKG name : the name of the current attribute name_len : length of the node's attribute name value : the value of the current value value_len : length of the node's attribute value */ struct mi_attr *add_mi_attr(struct mi_node *node, int flags, char *name, int name_len, char *value, int value_len)
struct mi_root *mi_debug(struct mi_root *cmd, void *param) { struct mi_root *rpl_tree; struct mi_node *node; char *p; int len; int new_debug; /* check the kids member of our root node - if the input root node has kids, our command was called with parameters */ node = cmd->node.kids; if (node!=NULL) { /* take the node's value and convert it to int, to make sure the parameter is valid */ if (str2sint( &node->value, &new_debug) < 0) /* if failed to convert to int, still return a RPL tree with an >=400 code and reason */ return init_mi_tree( 400, MI_SSTR(MI_BAD_PARM)); } else new_debug = *debug; /* all is good so far, initialize a new output ROOT tree which has a 200 OK code & reason */ rpl_tree = init_mi_tree( 200, MI_SSTR(MI_OK)); if (rpl_tree==0) return 0; p = sint2str((long)new_debug, &len); /* add a new node to our output tree, which the current debug level */ node = add_mi_node_child( &rpl_tree->node, MI_DUP_VALUE, MI_SSTR("DEBUG"),p, len); if (node==0) { free_mi_tree(rpl_tree); return 0; } /* if all was successful, overwrite the actual debug level, and return our tree */ *debug = new_debug; return rpl_tree; }
For more generic information on the MI Interface as well as some examples used for running MI commands with the opensipsctl utility, see the MI Interface documentation page. 11. Statistics APIOpenSIPS exposes a statistics API that can be used both from the core or the modules. The statistics are essentially counters that will be internally incremented/decremented by OpenSIPS and that can be fetched by the outside world ( via the MI interface ) for understanding the OpenSIPS load / health status / etc.
typedef struct stat_export_ { char* name; /* null terminated statistic name */ unsigned short flags; /* flags */ stat_var** stat_pointer; /* pointer to the variable's mem location * * NOTE - it's in shm mem */ } stat_export_t;
stat_var* rcv_reqs; stat_var* rcv_rpls; stat_var* fwd_reqs; stat_var* fwd_rpls; stat_var* drp_reqs; stat_var* drp_rpls; stat_var* err_reqs; stat_var* err_rpls; stat_var* bad_URIs; stat_var* unsupported_methods; stat_var* bad_msg_hdr; stat_export_t core_stats[] = { {"rcv_requests" , 0, &rcv_reqs }, {"rcv_replies" , 0, &rcv_rpls }, {"fwd_requests" , 0, &fwd_reqs }, {"fwd_replies" , 0, &fwd_rpls }, {"drop_requests" , 0, &drp_reqs }, {"drop_replies" , 0, &drp_rpls }, {"err_requests" , 0, &err_reqs }, {"err_replies" , 0, &err_rpls }, {"bad_URIs_rcvd", 0, &bad_URIs }, {"unsupported_methods", 0, &unsupported_methods }, {"bad_msg_hdr", 0, &bad_msg_hdr }, {"timestamp", STAT_IS_FUNC, (stat_var**)get_ticks }, {0,0,0} };
As note from the above structure, statistics can either be a simple counter ( eg. rcv_requests ), but it can also be a function. Statistics function might come in handy when the developer needs to do extra processing on the raw counters before providing the final output.
/* Parameters : module - a string describing the module the current statistics belong to. Will be used when fetching the statistics via MI stats - the statistics to be registered Returns : 0 in case of success, negative in case of error */ int register_module_stats(char *module, stat_export_t *stats;
Important to note here that all the above statistics related functions MUST be called in the context of the attendant process before forking is done.
/* Parameters : var : the statistics to be updated n : the value ( if positive -> stat will be increment. negative -> stat will be decremented ) */ void update_stat(stat_var* var, int n); /* Parameters : var : the statistics to be reseted */ void reset_stat(stat_var* var); /* Parameters : var : the statistics to be fetched Returns : statistic value */ unsigned long get_stat_val(stat_var* var) All statistics related code should be guarded by #ifdef STATISTICS , since the statistics are not a mandatory part of the OpenSIPS core ( they can be disabled from within menuconfig ).
For fetching the mynewstat statistic exported by the mynewmod module, one can use the opensipsctl like this : 12. SQL Database APIOpenSIPS exposes a SQL database API that the module developers can use for operating the most common SQL queries. Advantages here are :
/** * \brief Bind database module functions * * This function is special, it's only purpose is to call find_export function in * the core and find the addresses of all other database related functions. The * db_func_t callback given as parameter is updated with the found addresses. * * This function must be called before any other database API call! * * The database URL is of the form "mysql://username:password@host:port/database" or * "mysql" (database module name). * In the case of a database connection URL, this function looks only at the first * token (the database protocol). In the example above that would be "mysql": * \see db_func_t * \param mod database connection URL or a database module name * \param dbf database module callbacks to be further used * \return returns 0 if everything is OK, otherwise returns value < 0 */ int db_bind_mod(const str* mod, db_func_t* dbf); typedef struct db_func { unsigned int cap; /* Capability vector of the database transport */ db_use_table_f use_table; /* Specify table name */ db_init_f init; /* Initialize database connection */ db_close_f close; /* Close database connection */ db_query_f query; /* query a table */ db_fetch_result_f fetch_result; /* fetch result */ db_raw_query_f raw_query; /* Raw query - SQL */ db_free_result_f free_result; /* Free a query result */ db_insert_f insert; /* Insert into table */ db_delete_f delete; /* Delete from table */ db_update_f update; /* Update table */ db_replace_f replace; /* Replace row in a table */ db_last_inserted_id_f last_inserted_id; /* Retrieve the last inserted ID in a table */ db_insert_update_f insert_update; /* Insert into table, update on duplicate key */ } db_func_t; /* Example of usage below */ db_func_t sql_functions; db_url = str_init("mysql://root:vlad@localhost/opensips"); if (db_bind_mod(db_url, &sql_functions) < 0){ /* most likely the db_mysql modules was not loaded, or it was loaded after our module */ LM_ERR("Unable to bind to a database driver\n"); return -1; } After successfully binding to the module, the developer must also make sure that the URL provided from the script writer point of a back-end which also supports the capabilities that will be further used ( eg. when operating on a flat text file, the db_last_inserted_id_f function will not be populated, and thus if the C code calls that function, the module will crash ). This is done by using the DB_CAPABILITY macro : /** * Returns true if all the capabilities in cpv are supported by module * represented by dbf, false otherwise */ #define DB_CAPABILITY(dbf, cpv) (((dbf).cap & (cpv)) == (cpv)) /** * Represents the capabilities that a database driver supports. */ typedef enum db_cap { DB_CAP_QUERY = 1 << 0, /**< driver can perform queries */ DB_CAP_RAW_QUERY = 1 << 1, /**< driver can perform raw queries */ DB_CAP_INSERT = 1 << 2, /**< driver can insert data */ DB_CAP_DELETE = 1 << 3, /**< driver can delete data */ DB_CAP_UPDATE = 1 << 4, /**< driver can update data */ DB_CAP_REPLACE = 1 << 5, /**< driver can replace (also known as INSERT OR UPDATE) data */ DB_CAP_FETCH = 1 << 6, /**< driver supports fetch result queries */ DB_CAP_LAST_INSERTED_ID = 1 << 7, /**< driver can return the ID of the last insert operation */ DB_CAP_INSERT_UPDATE = 1 << 8, /**< driver can insert data into database and update on duplicate */ DB_CAP_MULTIPLE_INSERT = 1 << 9 /**< driver can insert multiple rows at once */ } db_cap_t; /** * All database capabilities except raw_query, replace, insert_update and * last_inserted_id which should be checked separately when needed */ #define DB_CAP_ALL (DB_CAP_QUERY | DB_CAP_INSERT | DB_CAP_DELETE | DB_CAP_UPDATE) /* Example of usage below */ if (!DB_CAPABILITY(sql_functions, DB_CAP_ALL)) { LM_CRIT("Database modules does not " "provide all functions needed by our module\n"); return -1; }
/** * \brief Initialize database connection and obtain the connection handle. * * This function initialize the database API and open a new database * connection. This function must be called after db_bind_mod but before any * other database API function is called. * * The function takes one parameter, the parameter must contain the database * connection URL. The URL is of the form * mysql://username:password\@host:port/database where: * * username: Username to use when logging into database (optional). * password: password if it was set (optional) * host: Hosname or IP address of the host where database server lives (mandatory) * port: Port number of the server if the port differs from default value (optional) * database: If the database server supports multiple databases, you must specify the * name of the database (optional). * \see bind_dbmod * \param _sqlurl database connection URL * \return returns a pointer to the db_con_t representing the connection if it was * successful, otherwise 0 is returned */ typedef db_con_t* (*db_init_f) (const str* _sqlurl); /* Example of usage below */ static db_con_t* db_connection; if ((db_connection = sql_functions.init(db_url)) == NULL) { LM_ERR("Failed to connect to the database \n"); return -1; }
Connection sharing between multiple processes does not work for the majority of back-end specific connectors ( eg. MySQL, Postgres, etc ). Due to this fact, the developers MUST make sure to create a sepparate database connection for each process that will eventually need one - in the context of Module development, the connections need to be opened in the child_init function. The output of the init() function will be the handler to be further used for all database interactions. When the connection is not needed anymore, the close method should be called : /** * \brief Close a database connection and free all memory used. * * The function closes previously open connection and frees all previously * allocated memory. The function db_close must be the very last function called. * \param _h db_con_t structure representing the database connection */ typedef void (*db_close_f) (db_con_t* _h);
/* Parameters : dbf - the functions to be used for running the version query dbh - the connection to run the version query table - str containing the table name we want to check for version version - the version we expect to find Returns : 0 means table version was successfully validated, negative in case of error ( internal error or older version found ) */ int db_check_table_version(db_func_t* dbf, db_con_t* dbh, const str* table, const unsigned int version);
/** * \brief Specify table name that will be used for subsequent operations. * * The function db_use_table takes a table name and stores it db_con_t structure. * All subsequent operations (insert, delete, update, query) are performed on * that table. * \param _h database connection handle * \param _t table name * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_use_table_f)(db_con_t* _h, const str * _t);
All queries must be preceded by a call to the use_table function. OpenSIPS internally does connection pooling - in case multiple module request connections to the same database, the connection will be shared between all those modules. Thus, in the context of a process, the same connection might be used by different modules - never assume a connection is dedicated to a single module. For running a SELECT query, you should use the query function. Prototype is : /** * \brief Query table for specified rows. * * This function implements the SELECT SQL directive. * If _k and _v parameters are NULL and _n is zero, you will get the whole table. * * if _c is NULL and _nc is zero, you will get all table columns in the result. * _r will point to a dynamically allocated structure, it is neccessary to call * db_free_result function once you are finished with the result. * * If _op is 0, equal (=) will be used for all key-value pairs comparisons. * * Strings in the result are not duplicated, they will be discarded if you call * db_free_result, make a copy yourself if you need to keep it after db_free_result. * * You must call db_free_result before you can call db_query again! * \see db_free_result * * \param _h database connection handle * \param _k array of column names that will be compared and their values must match * \param _op array of operators to be used with key-value pairs * \param _v array of values, columns specified in _k parameter must match these values * \param _c array of column names that you are interested in * \param _n number of key-value pairs to match in _k and _v parameters * \param _nc number of columns in _c parameter * \param _o order by statement for query * \param _r address of variable where pointer to the result will be stored * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_query_f) (const db_con_t* _h, const db_key_t* _k, const db_op_t* _op, const db_val_t* _v, const db_key_t* _c, const int _n, const int _nc, const db_key_t _o, db_res_t** _r);
/** * This type represents a result returned by db_query function (see below). The * result can consist of zero or more rows (see db_row_t description). * * Note: A variable of type db_res_t returned by db_query function uses dynamicaly * allocated memory, don't forget to call db_free_result if you don't need the * variable anymore. You will encounter memory leaks if you fail to do this! * * In addition to zero or more rows, each db_res_t object contains also an array * of db_key_t objects. The objects represent keys (names of columns). * */ typedef struct db_res { struct { db_key_t* names; /**< Column names */ db_type_t* types; /**< Column types */ int n; /**< Number of columns */ } col; struct db_row* rows; /**< Rows */ int n; /**< Number of rows in current fetch */ int res_rows; /**< Number of total rows in query */ int last_row; /**< Last row */ } db_res_t; /** * Structure holding the result of a query table function. * It represents one row in a database table. In other words, the row is an * array of db_val_t variables, where each db_val_t variable represents exactly * one cell in the table. */ typedef struct db_row { db_val_t* values; /**< Columns in the row */ int n; /**< Number of columns in the row */ } db_row_t; /** * This structure represents a value in the database. Several datatypes are * recognized and converted by the database API. These datatypes are automaticaly * recognized, converted from internal database representation and stored in the * variable of corresponding type. * * Module that want to use this values needs to copy them to another memory * location, because after the call to free_result there are not more available. * * If the structure holds a pointer to a string value that needs to be freed * because the module allocated new memory for it then the free flag must * be set to a non-zero value. A free flag of zero means that the string * data must be freed internally by the database driver. */ typedef struct { db_type_t type; /**< Type of the value */ int nul; /**< Means that the column in database has no value */ int free; /**< Means that the value should be freed */ /** Column value structure that holds the actual data in a union. */ union { int int_val; /**< integer value */ long long bigint_val; /**< big integer value */ double double_val; /**< double value */ time_t time_val; /**< unix time_t value */ const char* string_val; /**< zero terminated string */ str str_val; /**< str type string value */ str blob_val; /**< binary object data */ unsigned int bitmap_val; /**< Bitmap data type */ } val; } db_val_t;
/* Macros below work on result sets ( db_res_t ) /** Return the column names */ #define RES_NAMES(re) ((re)->col.names) /** Return the column types */ #define RES_TYPES(re) ((re)->col.types) /** Return the number of columns */ #define RES_COL_N(re) ((re)->col.n) /** Return the result rows */ #define RES_ROWS(re) ((re)->rows) /** Return the number of current result rows */ #define RES_ROW_N(re) ((re)->n) /** Return the last row of the result */ #define RES_LAST_ROW(re) ((re)->last_row) /** Return the number of total result rows */ #define RES_NUM_ROWS(re) ((re)->res_rows) /* Macros below work on rows */ /** Return the columns in the row */ #define ROW_VALUES(rw) ((rw)->values) /** Return the number of colums */ #define ROW_N(rw) ((rw)->n) /* Macros below work on values */ /** * Use this macro if you need to set/get the type of the value. */ #define VAL_TYPE(dv) ((dv)->type) /** * Use this macro if you need to set/get the null flag. A non-zero flag means that * the corresponding cell in the database contains no data (a NULL value in MySQL * terminology). */ #define VAL_NULL(dv) ((dv)->nul) /** * Use this macro if you need to access the integer value in the db_val_t structure. */ #define VAL_INT(dv) ((dv)->val.int_val) /** * Use this macro if you need to access the str structure in the db_val_t structure. */ #define VAL_STR(dv) ((dv)->val.str_val)
/* we will work on 'mytable' table with just two columns, keyname and value. The select query we will run is 'select value from mytable where keyname='abc';' */ db_key_t key; db_val_t val; db_key_t col; db_res_t* db_res = NULL; db_row_t * rows; db_val_t * values; #define KEY_COL "keyname" #define VALUE_COL "value" str key_column = str_init(KEY_COL); str value_column = str_init(VALUE_COL); str db_table = str_init("mytable"); val.type = DB_STR; val.nul = 0; val.val.str_val.s = "abc"; val.val.str_val.len = 3; key = &key_column; col = &value_column; if (sql_functions.use_table(db_handle, &db_table) < 0) { LM_ERR("sql use_table failed\n"); return -1; } if(sql_functions.query(db_handle, &key, NULL, &val, &col, 1, 1, NULL, &db_res) < 0) { LM_ERR("failed to query database\n"); return -1; } nr_rows = RES_ROW_N(db_res); rows = RES_ROWS(db_res); if (nr_rows <= 0) { LM_DBG("no rows found\n"); sql_functions.free_result(db_handle, db_res); return -1; } for (i=0;i<nr_rows;i++) { values = ROW_VALUES(rows + i); if (VAL_NULL(values)) { LM_WARN("Column value should not be null - skipping \n"); continue; } LM_DBG("We have feteched %s\n",VAL_STRING(values)); /* do further rows processing here */ } sql_functions.free_result(db_handle, db_res); return 0;
/** * \brief Free a result allocated by db_query. * * This function frees all memory allocated previously in db_query. Its * neccessary to call this function on a db_res_t structure if you don't need the * structure anymore. You must call this function before you call db_query again! * \param _h database connection handle * \param _r pointer to db_res_t structure to destroy * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_free_result_f) (db_con_t* _h, db_res_t* _r);
Sometimes, especially when querying large tables, it is not desirable to fetch all the rows in one chunk, since that might lead to the filling of the OpenSIPS private memory. /** * \brief Fetch a number of rows from a result. * * The function fetches a number of rows from a database result. If the number * of wanted rows is zero, the function returns anything with a result of zero. * \param _h structure representing database connection * \param _r structure for the result * \param _n the number of rows that should be fetched * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_fetch_result_f) (const db_con_t* _h, db_res_t** _r, const int _n);
/* check if our used DB driver supports fetching a limited number of rows */ if (DB_CAPABILITY(*dr_dbf, DB_CAP_FETCH)) { /* run our query as usual, but DO NOT provide a result set pointer ( last parameter 0 ) */ if ( dr_dbf->query( db_hdl, 0, 0, 0, columns, 0, db_cols, 0, 0 ) < 0) { LM_ERR("DB query failed\n"); goto error; } /* estimate how many rows we can fit into our current PKG memory */ no_rows = estimate_available_rows( 4+32+15+4+32+4+128+4+32+4, db_cols); if (no_rows==0) no_rows = 10; /* try to fetch our rows */ if(dr_dbf->fetch_result(db_hdl, &res, no_rows )<0) { LM_ERR("Error fetching rows\n"); goto error; } } else { /* no fetching rows support - fallback to full rows loading */ if ( dr_dbf->query(db_hdl,0,0,0,columns,0,db_cols,0,&res) < 0) { LM_ERR("DB query failed\n"); goto error; } } do { for(i=0; i < RES_ROW_N(res); i++) { row = RES_ROWS(res) + i; /* start processing our loaded rows */ } if (DB_CAPABILITY(*dr_dbf, DB_CAP_FETCH)) { /* any more rows to fetch ? */ if(dr_dbf->fetch_result(db_hdl, &res, no_rows)<0) { LM_ERR( "fetching rows (1)\n"); goto error; } /* success in fetching more rows - continue the loop */ } else { /* we were not supporting fetching rows in the first place, processed everything */ break; } } while(RES_ROW_N(res)>0); dr_dbf->free_result(db_hdl, res);
/** * \brief Insert a row into the specified table. * * This function implements INSERT SQL directive, you can insert one or more * rows in a table using this function. * \param _h database connection handle * \param _k array of keys (column names) * \param _v array of values for keys specified in _k parameter * \param _n number of keys-value pairs int _k and _v parameters * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_insert_f) (const db_con_t* _h, const db_key_t* _k, const db_val_t* _v, const int _n);
/** * \brief Delete a row from the specified table. * * This function implements DELETE SQL directive, it is possible to delete one or * more rows from a table. * If _k is NULL and _v is NULL and _n is zero, all rows are deleted, the * resulting table will be empty. * If _o is NULL, the equal operator "=" will be used for the comparison. * * \param _h database connection handle * \param _k array of keys (column names) that will be matched * \param _o array of operators to be used with key-value pairs * \param _v array of values that the row must match to be deleted * \param _n number of keys-value parameters in _k and _v parameters * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_delete_f) (const db_con_t* _h, const db_key_t* _k, const db_op_t* _o, const db_val_t* _v, const int _n);
/** * \brief Update some rows in the specified table. * * The function implements UPDATE SQL directive. It is possible to modify one * or more rows in a table using this function. * \param _h database connection handle * \param _k array of keys (column names) that will be matched * \param _o array of operators to be used with key-value pairs * \param _v array of values that the row must match to be modified * \param _uk array of keys (column names) that will be modified * \param _uv new values for keys specified in _k parameter * \param _n number of key-value pairs in _k and _v parameters * \param _un number of key-value pairs in _uk and _uv parameters * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_update_f) (const db_con_t* _h, const db_key_t* _k, const db_op_t* _o, const db_val_t* _v, const db_key_t* _uk, const db_val_t* _uv, const int _n, const int _un);
/** * \brief Insert a row and replace if one already exists. * * The function implements the REPLACE SQL directive. It is possible to insert * a row and replace if one already exists. The old row will be deleted before * the insertion of the new data. * \param _h structure representing database connection * \param _k key names * \param _v values of the keys * \param _n number of key=value pairs * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_replace_f) (const db_con_t* handle, const db_key_t* keys, const db_val_t* vals, const int n);
/** * \brief Retrieve the last inserted ID in a table. * * The function returns the value generated for an AUTO_INCREMENT column by the * previous INSERT or UPDATE statement. Use this function after you have * performed an INSERT statement into a table that contains an AUTO_INCREMENT * field. * \param _h structure representing database connection * \return returns the ID as integer or returns 0 if the previous statement * does not use an AUTO_INCREMENT value. */ typedef int (*db_last_inserted_id_f) (const db_con_t* _h);
/** * \brief Insert a row into specified table, update on duplicate key. * * The function implements the INSERT ON DUPLICATE KEY UPDATE SQL directive. * It is possible to insert a row and update if one already exists. * The old row will not deleted before the insertion of the new data. * \param _h structure representing database connection * \param _k key names * \param _v values of the keys * \param _n number of key=value pairs * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_insert_update_f) (const db_con_t* _h, const db_key_t* _k, const db_val_t* _v, const int _n);
/** * \brief Raw SQL query. * * This function can be used to do database specific queries. Please * use this function only if needed, as this creates portability issues * for the different databases. Also keep in mind that you need to * escape all external data sources that you use. You could use the * escape_common and unescape_common functions in the core for this task. * \see escape_common * \see unescape_common * \param _h structure representing database connection * \param _s the SQL query * \param _r structure for the result * \return returns 0 if everything is OK, otherwise returns value < 0 */ typedef int (*db_raw_query_f) (const db_con_t* _h, const str* _s, db_res_t** _r); 13. NoSQL API14. Event Interface API15. BIN Interface APIThe Binary Internal Interface is an OpenSIPS core interface which offers an efficient way for communication between individual OpenSIPS instances.
For creating and sending a new event, the following methods are to be used : /** * bin_init - begins the construction of a new binary packet (header part): * * +-------------------+------------------------------------------------------+ * | 8-byte HEADER | BODY max 65535 bytes | * +-------------------+------------------------------------------------------+ * | PK_MARKER | CRC | LEN | MOD_NAME | CMD | LEN | FIELD | LEN | FIELD |...| * +-------------------+------------------------------------------------------+ * * @param: { LEN, MOD_NAME } + CMD */ int bin_init(str *mod_name, int cmd_type) /* * copies the given string at the 'cpos' position in the buffer * allows null strings (NULL content or NULL param) * * @return: 0 on success */ int bin_push_str(const str *info) /* * adds a new integer value at the 'cpos' position in the buffer * * @return: 0 on success */ int bin_push_int(int info) /** * bin_send - computes the checksum of the current packet and then * sends the packet over UDP to the @dest destination * * @return: number of bytes sent, or -1 on error */ int bin_send(union sockaddr_union *dest)
/** * bin_register_cb - registers a module handler for specific packets * @mod_name: used to classify the incoming packets * @cb: the handler function, called once for each matched packet * * @return: 0 on success */ int bin_register_cb(char *mod_name, void (*cb)(int))
/* * pops an str from the current position in the buffer * @info: pointer to store the result * * @return: 0 on success * * Note: The pointer returned in @info str is only valid for the duration of * the callback. Don't forget to copy the info into a safe buffer! */ int bin_pop_str(str *info) /* * pops an integer value from the current position in the buffer * @info: pointer to store the result * * @return: 0 on success */ int bin_pop_int(void *info)
16. Module Development16.1 IntroductionDue to the OpenSIPS modular architecture, the easiest way to add new features ( new parameters, script functions, MI function etc ) is to incorporate them into a new OpenSIPS module. loadmodule "mynewmod.so"
struct module_exports{ char* name; /*!< null terminated module name */ char *version; /*!< module version */ char *compile_flags; /*!< compile flags used on the module */ unsigned int dlflags; /*!< flags for dlopen */ cmd_export_t* cmds; /*!< null terminated array of the exported commands */ param_export_t* params; /*!< null terminated array of the exported module parameters */ stat_export_t* stats; /*!< null terminated array of the exported module statistics */ mi_export_t* mi_cmds; /*!< null terminated array of the exported MI functions */ pv_export_t* items; /*!< null terminated array of the exported module items (pseudo-variables) */ proc_export_t* procs; /*!< null terminated array of the additional processes reqired by the module */ init_function init_f; /*!< Initialization function */ response_function response_f; /*!< function used for responses, returns yes or no; can be null */ destroy_function destroy_f; /*!< function called when the module should be "destroyed", e.g: on opensips exit */ child_init_function init_child_f;/*!< function called by all processes after the fork */ };
struct module_exports exports= { "dialog", /* module's name */ MODULE_VERSION, DEFAULT_DLFLAGS, /* dlopen flags */ cmds, /* exported functions */ mod_params, /* param exports */ mod_stats, /* exported statistics */ mi_cmds, /* exported MI functions */ mod_items, /* exported pseudo-variables */ 0, /* extra processes */ mod_init, /* module initialization function */ 0, /* reply processing function */ mod_destroy, child_init /* per-child init function */ }; 16.2 Compiling a moduleFurther on, we will be following the various options we have in building our new module, named ournewmod.
# $Id$ # # WARNING: do not run this directly, it should be run by the master Makefile include ../../Makefile.defs auto_gen= NAME=ournewmod.so LIBS= include ../../Makefile.modules
include ../../Makefile.defs auto_gen= NAME=cachedb_memcached.so DEFS+=-I$(LOCALBASE)/include LIBS=-L$(LOCALBASE)/lib -lmemcached include ../../Makefile.modules
If our new module depends on external libraries, the module must not be left to compile by default ! This must be done by editing the Makefile.conf.template file - where we specify which modules are to not be compiled by default, along with the dependencies they have. modulename= Module Description | module dependency
16.3 Initializing the moduleIn the context of initializing our new module, there are two types of functions that will help us : mod_initThis function must be specified in the init_f member of our module_exports exports structure. /* MUST return 0 in case of success, anything else in case of error */ typedef int (*init_function)(void);
Since this function is called from the context of only one process, after OpenSIPS forks, each OpenSIPS process will receive a copy of what the attendat process had. child_initThis function must be specified in the init_child_f member of our module_exports exports structure. /* MUST return 0 in case of success, anything else in case of error */ typedef int (*child_init_function)(int rank);
#define PROC_MAIN 0 /* Main opensips process */ #define PROC_TIMER -1 /* Timer attendant process */ #define PROC_MODULE -2 /* Extra process requested by modules */ #define PROC_TCP_MAIN -4 /* TCP main process */ #define PROC_BIN -8 /* Any binary interface listener */
If we must do time consuming operations ( eg. load many rows from a database ) , we should be doing this inside the child_init() function for a single process ( eg. rank == 1 would be the context of our first UDP listener) , instead of the mod_init() function.
16.4 Destroying the moduleThis function must be specified in the destroy_function member of our module_exports exports structure. typedef void (*destroy_function)(); 16.5 Adding module ParametersAdding new module parameters is done by populating the params member in our module's exports structure. At OpenSIPS startup, OpenSIPS will parse the provided script and set our internal variables accordingly to what the OpenSIPS script writer has configured. The parameter definition ( param_export_t ) is the following : struct param_export_ { char* name; /*!< null terminated param. name */ modparam_t type; /*!< param. type */ void* param_pointer; /*!< pointer to the param. memory location */ };
int enable_stats = 0; static str db_url = {NULL,0}; static param_export_t mod_params[]={ { "enable_stats", INT_PARAM, &enable_stats }, { "db_url", STR_PARAM, &db_url.s }, { 0,0,0 } }
loadmodule "ournewmod.so" modparam("ournewmod","enable_stats", 1) modparam("ournewmod","db_url","mysql://vlad:mypw@localhost/opensips")
static param_export_t params[]={ { "cachedb_url", STR_PARAM|USE_FUNC_PARAM, (void *)&set_connection}, {0,0,0} }; int set_connection(unsigned int type, void *val) { LM_INFO("Our parameter has been set : value is %s\n",(char *)val); /* continue processing, eg : add our new parameter to a list to be further processed */ } 16.6 Adding module FunctionsAdding new module parameters is done by populating the cmds member in our module's exports structure. struct cmd_export_ { char* name; /* null terminated command name */ cmd_function function; /* pointer to the corresponding function */ int param_no; /* number of parameters used by the function */ fixup_function fixup; /* pointer to the function called to "fix" the parameters */ free_fixup_function free_fixup; /* pointer to the function called to free the "fixed" parameters */ int flags; /* Function flags */ };
In order to overload a particular function, you can simply list it twice with the same name in the cmds structure, but change the param_no field. A script function exported by a module has the following definition : typedef int (*cmd_function)(struct sip_msg*, char*, char*, char*, char*, char*, char*);
The flags member in the cmd_export_ structure dictates where within the OpenSIPS script can that particular function be called. Current options here are : #define REQUEST_ROUTE 1 /*!< Request route block */ #define FAILURE_ROUTE 2 /*!< Negative-reply route block */ #define ONREPLY_ROUTE 4 /*!< Received-reply route block */ #define BRANCH_ROUTE 8 /*!< Sending-branch route block */ #define ERROR_ROUTE 16 /*!< Error-handling route block */ #define LOCAL_ROUTE 32 /*!< Local-requests route block */ #define STARTUP_ROUTE 64 /*!< Startup route block */ #define TIMER_ROUTE 128 /*!< Timer route block */ #define EVENT_ROUTE 256 /*!< Event route block */
{"lb_is_destination",(cmd_function)w_lb_is_dst4, 4, fixup_is_dst, 0, REQUEST_ROUTE|FAILURE_ROUTE|ONREPLY_ROUTE|BRANCH_ROUTE|LOCAL_ROUTE},
static int fixup_is_dst(void** param, int param_no) { if (param_no==1) { /* the ip to test */ return fixup_pvar(param); } else if (param_no==2) { /* the port to test */ if (*param==NULL) { return 0; } else if ( *((char*)*param)==0 ) { pkg_free(*param); *param = NULL; return 0; } return fixup_pvar(param); } else if (param_no==3) { /* the group to check in */ return fixup_igp(param); } else if (param_no==4) { /* active only check ? */ return fixup_uint(param); } else { LM_CRIT("bug - too many params (%d) in lb_is_dst()\n",param_no); return -1; } }
static int w_lb_is_dst4(struct sip_msg *msg,char *ip,char *port,char *grp, char *active) { int ret, group; if (fixup_get_ivalue(msg, (gparam_p)grp, &group) != 0) { LM_ERR("Invalid lb group pseudo variable!\n"); return -1; } ret = lb_is_dst(*curr_data, msg, (pv_spec_t*)ip, (pv_spec_t*)port, group, (int)(long)active);
The return code of the script exported functions from the module are very important. 16.7 Adding module MI FunctionsAdding new module MI functions is done by populating the mi_cmds member in our module's exports structure.
The MI functions in the mi_cmds member of the exports structure will be automatically registered by the module interface. 16.8 Adding module StatisticsAdding new module exported statistics is done by populating the stats member in our module's exports structure.
The statistics in the stats member of the exports structure will be automatically registered by the module interface.
If our new module named mynewmod exports a statistic called mycustomstat we will be able to fetch that statistic by using opensipsctl : 16.9 Adding module Pseudo-variablesAdding new module pseudo-variables is done by populating the items member in our module's exports structure. 16.10 Adding module dedicated ProcessesFor certain use cases, our module might need to talk to external entities which are not SIP based. struct proc_export_ { char *name; /* name of the new task */ mod_proc_wrapper pre_fork_function; /* function to be run before the fork */ mod_proc_wrapper post_fork_function; /* function to be run after the fork */ mod_proc function; /* actual function that will be run in the context of the new process */ unsigned int no; /* number of processes that will be forked to run the above function */ unsigned int flags; /* flags for our new processes - only PROC_FLAG_INITCHILD makes sense here*/ }; typedef void (*mod_proc)(int no); typedef int (*mod_proc_wrapper)();
The function that will run in the context of the new process must never terminate. Once the function exits, the entire OpenSIPS will stop.
They are both executed within the context of the attendant OpenSIPS process
The number of processes forked by OpenSIPS for a particular function is not neccesarily static.
static proc_export_t mi_procs[] = { {"MI Datagram", pre_datagram_process, post_datagram_process, datagram_process, MI_CHILD_NO, PROC_FLAG_INITCHILD }, {0,0,0,0,0,0} }; static param_export_t mi_params[] = { {"children_count", INT_PARAM, &mi_procs[0].no },
17. Module APIsWithin OpenSIPS, one modules might need to access the functionality of another module ( one very common example are modules desiring to do operations on a per dialog basis, thus needing part of the dialog module functionality ). Instead of directly accessing the functionality from within the target module, OpenSIPS heavily uses the concept of a 'module exported API'. 17.1 TM module17.2 RR ModuleThe RR ( Record Route ) module API is exported by the modules/rr/api.h file.
/* Parameters : rrb is the API output to be further used Returns : 0 in case of success and -1 in case of failure */ inline static int load_rr_api( struct rr_binds *rrb ); The rr_binds structure is exemplified below : struct rr_binds { add_rr_param_t add_rr_param; check_route_param_t check_route_param; is_direction_t is_direction; get_route_param_t get_route_param; register_rrcb_t register_rrcb; get_remote_target_t get_remote_target; get_route_set_t get_route_set; /* whether or not the append_fromtag parameter is enabled in the RR module */ int append_fromtag; /* the number of routes removed within the loose routing process */ int* removed_routes; /* the type of routing done, when comparing the previous and the next hop Both can be either strict or loose routers, thus here we have 4 different options : ROUTING_LL - loose to loose routing ROUTING_SL - strict to loose routing ROUTING_SS - strict to strict routing ROUTING_LS - loose to strict routing */ int* routing_type; loose_route_t loose_route; record_route_t record_route; };
/* Adds a parameter to the requests's Record-Route URI. The API supports the use case where the Record-Routed header will be further added. The function is to be used for marking certain dialogs that can be identified from the sequential requests - since the Route headers in the sequential requests will also contain our added params, which we'll be able to fetch with get_route_param ( see below ) The function returns 0 on success. Otherwise, -1 is returned. Parameters : * struct sip_msg* msg - request that will has the parameter “param” added to its Record-Route header. * str* param - parameter to be added to the Record-Route header - it must be in “;name=value” format. */ typedef int (*add_rr_param_t)(struct sip_msg* msg, str* param); /* The function checks for the request “msg” if the URI parameters of the local Route header (corresponding to the local server) matches the given regular expression “re”. It MUST be call after the loose_route was done. The function returns 0 on success. Otherwise, -1 is returned. * struct sip_msg* msg - request that will has the Route header parameters checked. * regex_t* re - compiled regular expression to be checked against the Route header parameters. */ int (*check_route_param_t)(struct sip_msg* msg, regex_t* rem); /* The function checks the flow direction of the request “msg”. As for checking it's used the “ftag” Route header parameter, the append_fromtag (see Section 1.4.1, “append_fromtag (integer)” module parameter must be enables. Also this must be call only after the loose_route is done. The function returns 0 if the “dir” is the same with the request's flow direction. Otherwise, -1 is returned. Meaning of the parameters is as follows: * struct sip_msg* msg - request that will have the direction checked. * int direction - direction to be checked against. It may be RR_FLOW_UPSTREAM ( from callee to caller ) or RR_FLOW_DOWNSTREAM ( from caller to callee ). */ typedef int (*is_direction_t)(struct sip_msg* msg, int direction); /* The function search in to the “msg”'s Route header parameters the parameter called “name” and returns its value into “val”. It must be call only after the loose_route is done. The function returns 0 if parameter was found (even if it has no value). Otherwise, -1 is returned. Meaning of the parameters is as follows: * struct sip_msg* msg - request that will have the Route header parameter searched. * str *name - contains the Route header parameter to be serached. * str *val - returns the value of the searched Route header parameter if found. It might be empty string if the parameter had no value. */ typedef int (*get_route_param_t)(struct sip_msg*, str*, str*); /* The function register a new callback (along with its parameter). The callback will be called when a loose route will succesfully be performed for the local address. The function returns 0 on success. Otherwise, -1 is returned. Meaning of the parameters is as follows: * rr_cb_t func - callback function to be registered. * void *param - parameter to be passed to the callback function. * short prior - parameter to set the priority. The callbacks will be executed in order from small to big priority - thus to be used for ordering callbacks that depend on each other. */ typedef int (*register_rrcb_t)( rr_cb_t func, void *param, short prior); /* Function to be registered as callback within the RR API : * struct sip_msg* req - request that is currently being processed * str *rr_param - the parameters in our server's Route header * str *param - the custom parameter provided at the callback registration */ typedef void (rr_cb_t) (struct sip_msg* req, str *rr_param, void *param); /* Function used to fetch the far-end remote target for the current message. Depending on the type routing done ( see the '''routing_type''' API member ) the remote target can be either in the initial Request URI, in the current Request-URI or in the last route header. The API function take care to correctly identify which scenario is correct. The API function MUST be called after loose_route() was called. The function returns the str pointer with the remote target, or NULL in case of error. Meaning of the parameters is as follows: -* struct sip_msg* msg - request that the remote target will be extracted from */ typedef str* (*get_remote_target_t)(struct sip_msg* msg); /* Function used to fetch the route set from the current SIP message. The function takes into account the actual loose_route() done, and properly discards the proxy's own Route headers from the SIP message. Thus, the function must be called after loose_route() was done. The function will return an array of str structures, or NULL in case of error. The nr_routes parameter will indicate the size of the returned array Meaning of the parameters is as follows: -* struct sip_msg* msg - request that the remote target will be extracted from -* int* nr_routes - the size of the returned array */ typedef str* (*get_route_set_t)(struct sip_msg*,int *nr_routes); /* Function to be used when for routing a request according to the Route headers present in it and to the type of Routing ( loose vs strict ) that needs to be used. The function will return 0 in case of success ( request is succesfully routed ). Otherwise, -1 is returned. Meaning of the parameters is as follows: -* struct sip_msg* msg - request to be routed */ typedef int (*loose_route_t)(struct sip_msg* msg); /* Function to be used when record-routing an initial request. The function will add one or two Record-Route headers , depending if there are any interface changes and if r2 is enabled. Also, if any parameters are provided, they will be added to all the Record-Route headers that the function internally adds. Returns 0 in case of success. Otherwise, -1 will be returned. Meaning of the parameters is as follows: -* struct sip_msg* msg - request to be record routed -* str* params - parameters to be added to the Record-Route headers */ typedef int (*record_route_t)(struct sip_msg* msg, str* params);
... #include "../rr/api.h" ... struct rr_binds my_rrb; ... ... int mod_init(void) { ... ... /* load the RR API */ if (load_rr_api( &my_rrb )!=0) { LM_ERR("can't load RR API\n"); goto error; } if (!my_rrb.append_fromtag) { LM_ERR("The append_fromtag parameter is not set, but we need it for detecting the direction of requests \n"); goto error; } ... ... /* register a RR callback */ if (my_rrb.register_rrcb(my_callback,0,0))!=0) { LM_ERR("can't register RR callback\n"); goto error; } ... ... } void my_callback(struct sip_msg* msg,str* rr_param,void *param) { str name = str_init("ftag"); str val; LM_INFO("Received a new sequential request from %s\n", my_rrb.is_direction( msg, RR_FLOW_UPSTREAM)?"callee":"caller"); if (my_rrb.get_route_param(msg,&name,&val) == 0) { LM_INFO("We have the ftag parameter with value [%.*s]\n",val.len,val.s); } }
18. Video TutorialA full video tutorial ( 7 video sessions of 1-2 hours ) going through the OpenSIPS development process can be found here , along with some source code examples used in the video tutorial. |