sbc: regex maps, flexible active profile selection

- regex maps are files with regex=>value entries which,
  once loaded, can be used with $M(key=>mapname) pattern replacement

- it is now possible to use replacements in active_profile, and
  also a list of profiles can be specified here - the first matching
  will be used
sayer/1.4-spce2.6
Stefan Sayer 15 years ago
parent ace0355f20
commit 8a9bca719a

@ -28,6 +28,8 @@
#include "log.h"
#include "AmSipHeaders.h"
#include "AmUtils.h"
#include "SBC.h" // for RegexMapper SBCFactory::regex_mappings
void replaceParsedParam(const string& s, size_t p,
AmUriParser& parsed, string& res) {
@ -273,6 +275,53 @@ string replaceParameters(const string& s,
skip_chars = skip_p-p;
} break;
case 'M': { // regex map
if (s[p+1] != '(') {
WARN("Error parsing $M regex map replacement (missing '(')\n");
break;
}
if (s.length()<p+3) {
WARN("Error parsing $M regex map replacement (short string)\n");
break;
}
size_t skip_p = p+2;
skip_p = skip_to_end_of_brackets(s, skip_p);
if (skip_p==s.length()) {
WARN("Error parsing $M regex map replacement (unclosed brackets)\n");
skip_chars = skip_p-p;
break;
}
string map_str = s.substr(p+2, skip_p-p-2);
size_t spos = map_str.rfind("=>");
if (spos == string::npos) {
skip_chars = skip_p-p;
WARN("Error parsing $M regex map replacement: no => found in '%s'\n",
map_str.c_str());
break;
}
string map_val = map_str.substr(0, spos);
string map_val_replaced = replaceParameters(map_val, r_type, req, app_param,
ruri_parser, from_parser, to_parser);
string mapping_name = map_str.substr(spos+2);
string map_res;
if (SBCFactory::regex_mappings.
mapRegex(mapping_name, map_val_replaced.c_str(), map_res)) {
DBG("matched regex mapping '%s' (orig '%s) in '%s'\n",
map_val_replaced.c_str(), map_val.c_str(), mapping_name.c_str());
res+=map_res;
} else {
DBG("no match in regex mapping '%s' (orig '%s') in '%s'\n",
map_val_replaced.c_str(), map_val.c_str(), mapping_name.c_str());
}
skip_chars = skip_p-p;
} break;
default: {
WARN("unknown replace pattern $%c%c\n",
s[p], s[p+1]);

@ -0,0 +1,65 @@
/*
* Copyright (C) 2011 Stefan Sayer
*
* This file is part of SEMS, a free SIP media server.
*
* SEMS 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.
*
* For a license to use the SEMS software under conditions
* other than those described here, or to purchase support for this
* software, please contact iptel.org by e-mail at the following addresses:
* info@iptel.org
*
* SEMS 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "RegexMapper.h"
#include "log.h"
bool RegexMapper::mapRegex(const string& mapping_name, const char* test_s,
string& result) {
lock();
std::map<string, RegexMappingVector>::iterator it=regex_mappings.find(mapping_name);
if (it == regex_mappings.end()) {
unlock();
ERROR("regex mapping '%s' is not loaded!\n", mapping_name.c_str());
return false;
}
bool res = run_regex_mapping(it->second, test_s, result);
unlock();
return res;
}
void RegexMapper::setRegexMap(const string& mapping_name, const RegexMappingVector& r) {
lock();
std::map<string, RegexMappingVector>::iterator it=regex_mappings.find(mapping_name);
if (it != regex_mappings.end()) {
for (RegexMappingVector::iterator r_it =
it->second.begin(); r_it != it->second.end(); r_it++) {
regfree(&r_it->first);
}
}
regex_mappings[mapping_name] = r;
unlock();
}
std::vector<std::string> RegexMapper::getNames() {
std::vector<std::string> res;
lock();
for (std::map<string, RegexMappingVector>::iterator it=
regex_mappings.begin(); it != regex_mappings.end(); it++)
res.push_back(it->first);
unlock();
return res;
}

@ -0,0 +1,55 @@
/*
* Copyright (C) 2011 Stefan Sayer
*
* This file is part of SEMS, a free SIP media server.
*
* SEMS 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.
*
* For a license to use the SEMS software under conditions
* other than those described here, or to purchase support for this
* software, please contact iptel.org by e-mail at the following addresses:
* info@iptel.org
*
* SEMS 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef _RegexMapper_h_
#define _RegexMapper_h_
#include "AmUtils.h"
#include <map>
#include <vector>
#include <string>
#include "AmThread.h"
struct RegexMapper {
RegexMapper() { }
~RegexMapper() { }
std::map<string, RegexMappingVector> regex_mappings;
AmMutex regex_mappings_mut;
void lock() { regex_mappings_mut.lock(); }
void unlock() { regex_mappings_mut.unlock(); }
bool mapRegex(const string& mapping_name, const char* test_s,
string& result);
void setRegexMap(const string& mapping_name, const RegexMappingVector& r);
std::vector<std::string> getNames();
};
#endif

@ -52,6 +52,7 @@ using std::map;
AmConfigReader SBCFactory::cfg;
AmSessionEventHandlerFactory* SBCFactory::session_timer_fact = NULL;
RegexMapper SBCFactory::regex_mappings;
EXPORT_MODULE_FACTORY(SBCFactory);
DEFINE_MODULE_INSTANCE(SBCFactory, MOD_NAME);
@ -89,15 +90,40 @@ int SBCFactory::onLoad()
}
}
active_profile = cfg.getParameter("active_profile");
if (active_profile != "$(paramhdr)" &&
active_profile != "$(ruri.user)" &&
call_profiles.find(active_profile) == call_profiles.end()) {
ERROR("call profile active_profile '%s' not loaded!\n", active_profile.c_str());
active_profile = explode(cfg.getParameter("active_profile"), ",");
if (active_profile.empty()) {
ERROR("active_profile not set.\n");
return -1;
}
INFO("SBC: active profile: '%s'\n", active_profile.c_str());
string active_profile_s;
for (vector<string>::iterator it =
active_profile.begin(); it != active_profile.end(); it++) {
if (it->empty())
continue;
if (((*it)[0] != '$') && call_profiles.find(*it) == call_profiles.end()) {
ERROR("call profile active_profile '%s' not loaded!\n", it->c_str());
return -1;
}
active_profile_s+=*it+",";
}
active_profile_s.erase(active_profile_s.length());
INFO("SBC: active profile: '%s'\n", active_profile_s.c_str());
vector<string> regex_maps = explode(cfg.getParameter("regex_maps"), ",");
for (vector<string>::iterator it =
regex_maps.begin(); it != regex_maps.end(); it++) {
string regex_map_file_name = AmConfig::ModConfigPath + *it + ".conf";
RegexMappingVector v;
if (!read_regex_mapping(regex_map_file_name, "=>",
("SBC regex mapping " + *it+":").c_str(), v)) {
ERROR("reading regex mapping from '%s'\n", regex_map_file_name.c_str());
return -1;
}
regex_mappings.setRegexMap(*it, v);
INFO("loaded regex mapping '%s'\n", it->c_str());
}
if (!AmPlugIn::registerApplication(MOD_NAME, this)) {
ERROR("registering "MOD_NAME" application\n");
@ -112,35 +138,58 @@ int SBCFactory::onLoad()
return 0;
}
#define REPLACE_VALS req, app_param, ruri_parser, from_parser, to_parser
/** get the first matching profile name from active profiles */
string SBCFactory::getActiveProfileMatch(string& profile_rule, const AmSipRequest& req,
const string& app_param, AmUriParser& ruri_parser,
AmUriParser& from_parser, AmUriParser& to_parser) {
string res;
for (vector<string>::iterator it=
active_profile.begin(); it != active_profile.end(); it++) {
if (it->empty())
continue;
if (*it == "$(paramhdr)")
res = get_header_keyvalue(app_param,"profile");
else if (*it == "$(ruri.user)")
res = req.user;
else
res = replaceParameters(*it, "active_profile", REPLACE_VALS);
if (!res.empty()) {
profile_rule = *it;
break;
}
}
return res;
}
AmSession* SBCFactory::onInvite(const AmSipRequest& req)
{
AmUriParser ruri_parser, from_parser, to_parser;
profiles_mut.lock();
string app_param = getHeader(req.hdrs, PARAM_HDR, true);
string profile = active_profile;
if (profile == "$(paramhdr)") {
profile = get_header_keyvalue(app_param,"profile");
} else if (profile == "$(ruri.user)") {
profile = req.user;
}
string profile_rule;
string profile = getActiveProfileMatch(profile_rule, REPLACE_VALS);
map<string, SBCCallProfile>::iterator it=
call_profiles.find(profile);
if (it==call_profiles.end()) {
profiles_mut.unlock();
ERROR("could not find call profile '%s' (active_profile = %s)\n",
profile.c_str(), active_profile.c_str());
ERROR("could not find call profile '%s' (matching active_profile rule: '%s')\n",
profile.c_str(), profile_rule.c_str());
throw AmSession::Exception(500,SIP_REPLY_SERVER_INTERNAL_ERROR);
}
DBG("using call profile '%s'\n", profile.c_str());
DBG("using call profile '%s' (from matching active_profile rule '%s')\n",
profile.c_str(), profile_rule.c_str());
SBCCallProfile& call_profile = it->second;
if (!call_profile.refuse_with.empty()) {
AmUriParser ruri_parser, from_parser, to_parser;
#define REPLACE_VALS req, app_param, ruri_parser, from_parser, to_parser
string refuse_with = replaceParameters(call_profile.refuse_with,
"refuse_with", REPLACE_VALS);
@ -174,7 +223,6 @@ AmSession* SBCFactory::onInvite(const AmSipRequest& req)
AmSipDialog::reply_error(req, refuse_with_code, refuse_with_reason, hdrs);
return NULL;
#undef REPLACE_VALS
}
AmConfigReader& sst_cfg = call_profile.use_global_sst_config ?
@ -234,6 +282,11 @@ void SBCFactory::invoke(const string& method, const AmArg& args,
} else if (method == "setActiveProfile"){
args.assertArrayFmt("u");
setActiveProfile(args,ret);
} else if (method == "getRegexMapNames"){
getRegexMapNames(args,ret);
} else if (method == "setRegexMap"){
args.assertArrayFmt("u");
setRegexMap(args,ret);
} else if(method == "_list"){
ret.push(AmArg("listProfiles"));
ret.push(AmArg("reloadProfiles"));
@ -241,6 +294,8 @@ void SBCFactory::invoke(const string& method, const AmArg& args,
ret.push(AmArg("loadProfile"));
ret.push(AmArg("getActiveProfile"));
ret.push(AmArg("setActiveProfile"));
ret.push(AmArg("getRegexMapNames"));
ret.push(AmArg("setRegexMap"));
} else
throw AmDynInvoke::NotImplemented(method);
}
@ -364,7 +419,10 @@ void SBCFactory::loadProfile(const AmArg& args, AmArg& ret) {
void SBCFactory::getActiveProfile(const AmArg& args, AmArg& ret) {
profiles_mut.lock();
AmArg p;
p["active_profile"] = active_profile;
for (vector<string>::iterator it=active_profile.begin();
it != active_profile.end(); it++) {
p["active_profile"].push(*it);
}
profiles_mut.unlock();
ret.push(200);
ret.push("OK");
@ -374,11 +432,11 @@ void SBCFactory::getActiveProfile(const AmArg& args, AmArg& ret) {
void SBCFactory::setActiveProfile(const AmArg& args, AmArg& ret) {
if (!args[0].hasMember("active_profile")) {
ret.push(400);
ret.push("Parameters error: expected ['active_profile': <active_profile>] ");
ret.push("Parameters error: expected ['active_profile': <active_profile list>] ");
return;
}
profiles_mut.lock();
active_profile = args[0]["active_profile"].asCStr();
active_profile = explode(args[0]["active_profile"].asCStr(), ",");
profiles_mut.unlock();
ret.push(200);
ret.push("OK");
@ -387,6 +445,39 @@ void SBCFactory::setActiveProfile(const AmArg& args, AmArg& ret) {
ret.push(p);
}
void SBCFactory::getRegexMapNames(const AmArg& args, AmArg& ret) {
AmArg p;
vector<string> reg_names = regex_mappings.getNames();
for (vector<string>::iterator it=reg_names.begin();
it != reg_names.end(); it++) {
p["regex_maps"].push(*it);
}
ret.push(200);
ret.push("OK");
ret.push(p);
}
void SBCFactory::setRegexMap(const AmArg& args, AmArg& ret) {
if (!args[0].hasMember("name") || !args[0].hasMember("file") ||
!isArgCStr(args[0]["name"]) || !isArgCStr(args[0]["file"])) {
ret.push(400);
ret.push("Parameters error: expected ['name': <name>, 'file': <file name>]");
return;
}
string m_name = args[0]["name"].asCStr();
string m_file = args[0]["file"].asCStr();
RegexMappingVector v;
if (!read_regex_mapping(m_file, "=>", "SBC regex mapping", v)) {
ERROR("reading regex mapping from '%s'\n", m_file.c_str());
ret.push(401);
ret.push("Error reading regex mapping from file");
return;
}
regex_mappings.setRegexMap(m_name, v);
ret.push(200);
ret.push("OK");
}
SBCDialog::SBCDialog(const SBCCallProfile& call_profile)
: m_state(BB_Init),
@ -408,8 +499,6 @@ void SBCDialog::onInvite(const AmSipRequest& req)
DBG("processing initial INVITE\n");
#define REPLACE_VALS req, app_param, ruri_parser, from_parser, to_parser
string app_param = getHeader(req.hdrs, PARAM_HDR, true);
if(dlg.reply(req, 100, "Connecting") != 0) {

@ -32,6 +32,7 @@
#include "AmUriParser.h"
#include "HeaderFilter.h"
#include "SBCCallProfile.h"
#include "RegexMapper.h"
#include <map>
@ -48,7 +49,7 @@ class SBCFactory: public AmSessionFactory,
std::map<string, SBCCallProfile> call_profiles;
string active_profile;
vector<string> active_profile;
AmMutex profiles_mut;
void listProfiles(const AmArg& args, AmArg& ret);
@ -57,6 +58,12 @@ class SBCFactory: public AmSessionFactory,
void loadProfile(const AmArg& args, AmArg& ret);
void getActiveProfile(const AmArg& args, AmArg& ret);
void setActiveProfile(const AmArg& args, AmArg& ret);
void getRegexMapNames(const AmArg& args, AmArg& ret);
void setRegexMap(const AmArg& args, AmArg& ret);
string getActiveProfileMatch(string& profile_rule, const AmSipRequest& req,
const string& app_param, AmUriParser& ruri_parser,
AmUriParser& from_parser, AmUriParser& to_parser);
public:
DECLARE_MODULE_INSTANCE(SBCFactory);
@ -70,6 +77,8 @@ class SBCFactory: public AmSessionFactory,
static AmConfigReader cfg;
static AmSessionEventHandlerFactory* session_timer_fact;
static RegexMapper regex_mappings;
// DI
// DI factory
AmDynInvoke* getInstance() { return instance(); }
@ -166,6 +175,6 @@ class SBCCalleeSession
void setAuthHandler(AmSessionEventHandler* h) { auth = h; }
};
static void assertEndCRLF(string& s);
extern void assertEndCRLF(string& s);
#endif

@ -5,7 +5,7 @@
# path (the path where this file resides)
profiles=transparent,auth_b2b,sst_b2b
# active call profile
# active call profile - comma separated list, first non-empty is used
#
# o active_profile=<profile_name> always use <profile_name>
#
@ -13,8 +13,16 @@ profiles=transparent,auth_b2b,sst_b2b
#
# o active_profile=$(paramhdr) use "profile" option in P-App-Param header
#
# o any replacement pattern
#
active_profile=transparent
# regex_maps - comma-separated list of regex maps to load at startup, for $M()
#
# regex=>value maps for which names are given here are loaded from
# this path, e.g. src_ipmap.conf, ruri_map.conf, usermap.conf
#
#regex_maps=src_ipmap,ruri_map,usermap
## RFC4028 Session Timer
# default configuration - can be overridden by call profiles
@ -39,8 +47,6 @@ active_profile=transparent
#
# Default: UPDATE_FALLBACK_INVITE
#
# Note: Session Timers are only supported in some applications
#
#session_refresh_method=UPDATE
# accept_501_reply - accept 501 reply as successful refresh? [yes|no]

@ -0,0 +1,7 @@
# this is a sample regex mapping to map source address 10.0.* to internal1 profile
# and 10.1.* to internal2 profile.
# For example, used with this active_profile setting:
# active_profile=$M($si=>src_ipmap),refuse
# all other calls would be blocked.
^10\.0\..*=>internal1
^10\.1\..*=>internal2

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from xmlrpclib import *
s = ServerProxy('http://localhost:8090')
print s.getRegexMapNames()

@ -0,0 +1,13 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from xmlrpclib import *
if len(sys.argv) != 3:
print "usage: %s <regex map name> <full regex map path>" % sys.argv[0]
sys.exit(1)
s = ServerProxy('http://localhost:8090')
print "Active calls: %d" % s.calls()
p ={ 'name' : sys.argv[1], 'file' : sys.argv[2] }
print s.setRegexMap(p)

@ -951,7 +951,7 @@ string get_header_keyvalue(const string& param_hdr, const string& short_name, co
}
/**
* get value from parameter header with the name @param name
* get value from parameter header with the name @param name
* while skipping escaped values
*/
string get_header_keyvalue(const string& param_hdr, const string& name) {
@ -1217,6 +1217,26 @@ void add_env_path(const char* name, const string& path)
#endif
}
/** skip to the end of a string enclosed in round brackets, skipping more
bracketed items, too */
size_t skip_to_end_of_brackets(const string& s, size_t start) {
size_t res = start;
char last_c = ' ';
int num_brackets = 0;
while (res < s.size() &&
(s[res] != ')' || num_brackets || last_c == '\\')) {
if (last_c != '\\') {
if (s[res]==')' && num_brackets)
num_brackets--;
else if (s[res]=='(')
num_brackets++;
}
last_c = s[res];
res++;
}
return res;
}
bool read_regex_mapping(const string& fname, const char* separator,
const char* dbg_type,
RegexMappingVector& result) {

@ -343,6 +343,9 @@ std::vector<string> explode(const string& s, const string& delim, const bool kee
/** add a directory to an environement variable */
void add_env_path(const char* name, const string& path);
size_t skip_to_end_of_brackets(const string& s, size_t start);
typedef std::vector<std::pair<regex_t, string> > RegexMappingVector;
/** read a regex=>string mapping from file

@ -29,16 +29,46 @@ SBC Profiles
All features are set in an SBC profile, which is configured in a separate
configuration file with the extension .sbcprofile.conf. Several SBC profiles
may be loaded at startup (load_profiles), and can be selected with the
active_profile configuration option
active_profile configuration option. The active_profile option is a comma-separated
list, the first profile that matches, i.e. is non-empty, will be used.
In this list a profile may be selected
o statically (active_profile=<profile_name>)
o depending on user part of INVITE Request URI(active_profile=$(ruri.user))
o depending on user part of INVITE Request URI (active_profile=$(ruri.user))
o depending on "profile" option in P-App-Param header (active_profile=$(paramhdr))
By using the latter two options, the SBC profile for the call can be selected in the
proxy.
o using any replacement pattern (see below), especially regex maps $M(val=>map)
By using the latter options, the SBC profile for the call can also be selected in
the proxy.
Examples:
active_profile=auth_b2b
active_profile=$(paramhdr),refuse
active_profile=$M($si=>ipmap),$(P-SBCProfile),refuse
Example:
In order to have all calls coming from source IP 10.0.* going to
'internal1' profile, all calls coming from source IP 10.1.* going to 'internal2'
profile, then for calls coming from other IP addresses those to RURI-domain
iptel.org go to 'iptel' profile, and all other calls being refused, we could set
~~~~~~~~~ sbc.conf ~~~~~~~~~
profiles=internal1,internal2,iptel,refuse
regex_maps=src_ipmap,rurimap
active_profile=$M($si=>src_ipmap),$M($rh=>rurimap),refuse
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~
~~~~~~~~~ src_ipmap.conf ~~~
^10\.0\..*=>internal1
^10\.1\..*=>internal2
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~ rurimap.conf ~~~~~
iptel.org=>iptel
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SBC profile reload
------------------
@ -65,11 +95,11 @@ is loaded. The expected parameters to all functions are in a dictionary with
'name' : profile name
'path' : profile conf file path
'active_profile': active profile (string)
Return code is [200, "OK", <result dictionary>] on success, or [<error code>, <error reason>]
on failure.
Return code is [200, "OK", <result dictionary>] on success, or
[<error code>, <error reason>] on failure.
RURI, From, To, etc - Replacement patterns
-------------------------------------
Replacement patterns - active_profile, RURI, From, To, etc
----------------------------------------------------------
In SBC profile the appearance of the outgoing INVITE request can be set,
by setting RURI, From and To parameters. If any of those parameters is not
set, the corresponding value of the incoming request is used.
@ -152,6 +182,10 @@ The patterns which can be used are the following:
next_hop_port=$Hp(P-SomeNH-URI)
$M(value=>regexmap) - map a value (any pattern) to a regexmap (see below)
Example: $M($fU=>usermap)
\\ -> \
\$ -> $
\* -> *
@ -168,6 +202,31 @@ If a space is contained, use quotation at the beginning and end.
Example:
To="\"someone\" <$aU@mytodomain.com>"
Regex mappings ($M(key=>map))
-----------------------------
A regex mapping is a (sorted) list of "regular expression" => "string value" pairs.
The regex mapping is executed with a key - any string, replacement pattern or
combination - and the first regular expression that matches returns the "string value".
Regex mappings are read from a text file, where each line corresponds to one
regex=>value pair. The mappings to load on startup are set with the regex_maps
config option, the file name from where it is loaded is "<mapping name>.conf" in
the plugin config path.
Mappings can also loaded into the running server by using the setRegexMap DI function
or the included sems-sbc-*-regex-* scripts:
sems-sbc-set-regex-map <name> <file> load a regex map from a file
sems-sbc-get-regex-map-names list regex map names
Example regex map:
~~~~~~~ usermap.conf ~~~~~~
# this is a comment
^stefan=>stefansayer
^frank=>frankmajer
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Setting Call-ID
---------------
For debugging purposes, the call-id of the outgoing leg can be set to depend on

Loading…
Cancel
Save