initial checkin of the sbc module

The SBC application is a highly flexible high-performance Back-to-Back
User Agent (B2BUA). It can be employed for a variety of uses, for example
topology hiding, From/To modification, enforcing SIP Session Timers,
identity change, SIP authentication. Future uses include accounting,
call timers, RTP call bridging, transcoding, call distribution.
sayer/1.4-spce2.6
Stefan Sayer 15 years ago
parent 17502bbcde
commit f86ac8e0ff

@ -0,0 +1,6 @@
set (sbc_SRCS
SBC.cpp
)
SET(sems_module_name sbc)
INCLUDE(${CMAKE_SOURCE_DIR}/cmake/module.rules.txt)

@ -0,0 +1,178 @@
/*
* Copyright (C) 2010 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 "HeaderFilter.h"
#include "sip/parse_common.h"
#include "log.h"
const char* FilterType2String(FilterType ft) {
switch(ft) {
case Transparent: return "transparent";
case Whitelist: return "whitelist";
case Blacklist: return "blacklist";
default: return "unknown";
};
}
int skip_header(const std::string& hdr, size_t start_pos,
size_t& name_end, size_t& val_begin,
size_t& val_end, size_t& hdr_end) {
// adapted from sip/parse_header.cpp
name_end = val_begin = val_end = start_pos;
hdr_end = hdr.length();
//
// Header states
//
enum {
H_NAME=0,
H_HCOLON,
H_VALUE_SWS,
H_VALUE,
};
int st = H_NAME;
int saved_st = 0;
size_t p = start_pos;
for(;p<hdr.length() && st != ST_LF && st != ST_CRLF;p++){
switch(st){
case H_NAME:
switch(hdr[p]){
case_CR_LF;
case HCOLON:
st = H_VALUE_SWS;
name_end = p;
break;
case SP:
case HTAB:
st = H_HCOLON;
name_end = p;
break;
}
break;
case H_VALUE_SWS:
switch(hdr[p]){
case_CR_LF;
case SP:
case HTAB:
break;
default:
st = H_VALUE;
val_begin = p;
break;
};
break;
case H_VALUE:
switch(hdr[p]){
case_CR_LF;
};
if (st==ST_CR || st==ST_LF)
val_end = p;
break;
case H_HCOLON:
switch(hdr[p]){
case HCOLON:
st = H_VALUE_SWS;
val_begin = p;
break;
case SP:
case HTAB:
break;
default:
DBG("Missing ':' after header name\n");
return MALFORMED_SIP_MSG;
}
break;
case_ST_CR(hdr[p]);
st = saved_st;
hdr_end = p;
break;
}
}
hdr_end = p;
if (p==hdr.length() && st==H_VALUE) {
val_end = p;
}
return 0;
}
int inplaceHeaderFilter(string& hdrs, const set<string>& headerfilter_list, FilterType f_type) {
if (!hdrs.length() || f_type == Transparent)
return 0;
int res = 0;
size_t start_pos = 0;
while (start_pos<hdrs.length()) {
size_t name_end, val_begin, val_end, hdr_end;
if ((res = skip_header(hdrs, start_pos, name_end, val_begin,
val_end, hdr_end)) != 0) {
return res;
}
string hdr_name = hdrs.substr(start_pos, name_end-start_pos);
bool erase = false;
if (f_type == Whitelist) {
erase = headerfilter_list.find(hdr_name)==headerfilter_list.end();
} else if (f_type == Blacklist) {
erase = headerfilter_list.find(hdr_name)!=headerfilter_list.end();
}
if (erase) {
DBG("erasing header '%s'\n", hdr_name.c_str());
hdrs.erase(start_pos, hdr_end-start_pos);
} else {
start_pos = hdr_end;
}
}
// todo: multi-line header support
return res;
}
/** EMACS **
* Local variables:
* mode: c++
* c-basic-offset: 4
* End:
*/

@ -0,0 +1,41 @@
/*
* Copyright (C) 2010 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 _HeaderFilter_h_
#define _HeaderFilter_h_
#include <string>
using std::string;
#include <set>
using std::set;
enum FilterType { Transparent=0, Whitelist, Blacklist };
const char* FilterType2String(FilterType ft);
int skip_header(const std::string& hdr, size_t start_pos,
size_t& name_end, size_t& val_begin, size_t& val_end, size_t& hdr_end);
int inplaceHeaderFilter(string& hdrs, const set<string>& headerfilter_list,
FilterType f_type);
#endif

@ -0,0 +1,7 @@
plug_in_name = sbc
module_ldflags =
module_cflags = -DMOD_NAME=\"$(plug_in_name)\"
COREPATH ?= ../../core
include $(COREPATH)/plug-in/Makefile.app_module

@ -0,0 +1,762 @@
/*
* $Id: SBC.cpp 1784 2010-04-15 13:01:00Z sayer $
*
* Copyright (C) 2010 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
*/
/*
SBC - feature-wishlist
- SDP filter (reconstructed SDP)
- maximum call duration timer
- accounting (MySQL DB, cassandra DB)
- RTP forwarding mode (bridging)
- RTP transcoding mode (bridging)
- overload handling (parallel call to target thresholds)
- call distribution
- select profile on monitoring in-mem DB record
- fallback profile
- add headers
*/
#include "SBC.h"
#include "log.h"
#include "AmUtils.h"
#include "AmAudio.h"
#include "AmPlugIn.h"
#include "AmMediaProcessor.h"
#include "AmConfigReader.h"
#include "AmSessionContainer.h"
#include "AmSipHeaders.h"
#include "HeaderFilter.h"
using std::map;
string SBCFactory::user;
string SBCFactory::domain;
string SBCFactory::pwd;
AmConfigReader SBCFactory::cfg;
AmSessionEventHandlerFactory* SBCFactory::session_timer_fact = NULL;
EXPORT_SESSION_FACTORY(SBCFactory,MOD_NAME);
bool SBCCallProfile::readFromConfiguration(const string& name, const string profile_file_name) {
if (cfg.loadFile(profile_file_name)) {
ERROR("reading SBC call profile from '%s'\n", profile_file_name.c_str());
return false;
}
string hf_type = cfg.getParameter("header_filter", "transparent");
if (hf_type=="transparent")
headerfilter = Transparent;
else if (hf_type=="whitelist")
headerfilter = Whitelist;
else if (hf_type=="blacklist")
headerfilter = Blacklist;
else {
ERROR("invalid header_filter mode '%s'\n", hf_type.c_str());
return false;
}
vector<string> elems = explode(cfg.getParameter("header_list"), ",");
for (vector<string>::iterator it=elems.begin(); it != elems.end(); it++)
headerfilter_list.insert(*it);
string mf_type = cfg.getParameter("message_filter", "transparent");
if (mf_type=="transparent")
messagefilter = Transparent;
else if (mf_type=="whitelist")
messagefilter = Whitelist;
else if (hf_type=="blacklist")
messagefilter = Blacklist;
else {
ERROR("invalid message_filter mode '%s'\n", mf_type.c_str());
return false;
}
elems = explode(cfg.getParameter("message_list"), ",");
for (vector<string>::iterator it=elems.begin(); it != elems.end(); it++)
messagefilter_list.insert(*it);
sst_enabled = cfg.getParameter("enable_session_timer", "no") == "yes";
use_global_sst_config = !cfg.hasParameter("session_expires");
auth_enabled = cfg.getParameter("enable_auth", "no") == "yes";
auth_credentials.user = cfg.getParameter("auth_user");
auth_credentials.pwd = cfg.getParameter("auth_pwd");
ruri = cfg.getParameter("RURI");
from = cfg.getParameter("From");
to = cfg.getParameter("To");
INFO("SBC: loaded SBC profile '%s':\n", name.c_str());
INFO("SBC: RURI = '%s'\n", ruri.c_str());
INFO("SBC: From = '%s'\n", from.c_str());
INFO("SBC: To = '%s'\n", to.c_str());
INFO("SBC: header filter is %s, %zd items in list\n",
FilterType2String(headerfilter), headerfilter_list.size());
INFO("SBC: message filter is %s, %zd items in list\n",
FilterType2String(messagefilter), messagefilter_list.size());
INFO("SBC: SST %sabled\n", sst_enabled?"en":"dis");
INFO("SBC: SIP auth %sabled\n", auth_enabled?"en":"dis");
return true;
}
SBCFactory::SBCFactory(const string& _app_name)
: AmSessionFactory(_app_name)
{
}
int SBCFactory::onLoad()
{
if(cfg.loadFile(AmConfig::ModConfigPath + string(MOD_NAME ".conf"))) {
ERROR("No configuration for sbc present (%s)\n",
(AmConfig::ModConfigPath + string(MOD_NAME ".conf")).c_str()
);
return -1;
}
session_timer_fact = AmPlugIn::instance()->getFactory4Seh("session_timer");
if(!session_timer_fact) {
ERROR("could not load session_timer from session_timer plug-in\n");
return -1;
}
vector<string> profiles_names = explode(cfg.getParameter("profiles"), ",");
for (vector<string>::iterator it =
profiles_names.begin(); it != profiles_names.end(); it++) {
string profile_file_name = AmConfig::ModConfigPath + *it + ".sbcprofile.conf";
if (!call_profiles[*it].readFromConfiguration(*it, profile_file_name)) {
ERROR("configuring SBC call profile from '%s'\n", profile_file_name.c_str());
return -1;
}
}
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());
return -1;
}
INFO("SBC: active profile: '%s'\n", active_profile.c_str());
return 0;
}
AmSession* SBCFactory::onInvite(const AmSipRequest& req)
{
string profile = active_profile;
if (profile == "$(paramhdr)") {
string app_param = getHeader(req.hdrs, PARAM_HDR, true);
profile = get_header_keyvalue(app_param,"profile");
} else if (profile == "$(ruri.user)") {
profile = req.user;
}
map<string, SBCCallProfile>::iterator it=
call_profiles.find(profile);
if (it==call_profiles.end()) {
ERROR("could not find call profile '%s' (active_profile = %s)\n",
profile.c_str(), active_profile.c_str());
throw AmSession::Exception(500,"Server Internal Error");
}
DBG("using call profile '%s'\n", profile.c_str());
SBCCallProfile& call_profile = it->second;
AmConfigReader& sst_cfg = call_profile.use_global_sst_config ?
cfg : call_profile.cfg; // override with profile config
if (call_profile.sst_enabled) {
DBG("Enabling SIP Session Timers\n");
if (!session_timer_fact->onInvite(req, sst_cfg))
return NULL;
}
SBCDialog* b2b_dlg = new SBCDialog(call_profile);
if (call_profile.sst_enabled) {
AmSessionEventHandler* h = session_timer_fact->getHandler(b2b_dlg);
if(!h) {
delete b2b_dlg;
ERROR("could not get a session timer event handler\n");
throw AmSession::Exception(500,"Server internal error");
}
if (h->configure(sst_cfg)){
ERROR("Could not configure the session timer: disabling session timers.\n");
delete h;
} else {
b2b_dlg->addHandler(h);
}
}
return b2b_dlg;
}
SBCDialog::SBCDialog(const SBCCallProfile& call_profile) // AmDynInvoke* user_timer)
: m_state(BB_Init),
call_profile(call_profile)
{
set_sip_relay_only(false);
}
SBCDialog::~SBCDialog()
{
}
void SBCDialog::replaceParsedParam(const string& s, size_t p,
AmUriParser& parsed, string& res) {
switch (s[p+1]) {
case 'u': { // URI
res+=parsed.uri_user+"@"+parsed.uri_host;
if (!parsed.uri_port.empty())
res+=":"+parsed.uri_port;
} break;
case 'U': res+=parsed.uri_user; break; // User
case 'd': { // domain
res+=parsed.uri_host;
if (!parsed.uri_port.empty())
res+=":"+parsed.uri_port;
} break;
case 'h': res+=parsed.uri_host; break; // host
case 'p': res+=parsed.uri_port; break; // port
case 'H': res+=parsed.uri_headers; break; // Headers
case 'P': res+=parsed.uri_param; break; // Params
default: WARN("unknown replace pattern $%c%c\n",
s[p], s[p+1]); break;
};
}
string SBCDialog::replaceParameters(const char* r_type,
const string& s, const AmSipRequest& req,
const string& app_param,
AmUriParser& ruri_parser, AmUriParser& from_parser,
AmUriParser& to_parser) {
string res;
bool is_replaced = false;
size_t p = 0;
char last_char=' ';
while (p<s.length()) {
size_t skip_chars = 1;
if (last_char=='\\') {
res += s[p];
is_replaced = true;
} else if (s[p]=='\\') {
if (p==s.length()-1)
res += s[p];
} else if ((s[p]=='$') && (s.length() >= p+1)) {
is_replaced = true;
p++;
switch (s[p]) {
case 'f': { // from
if ((s.length() == p+1) || (s[p+1] == '.')) {
res += req.from;
break;
}
if (from_parser.uri.empty()) {
from_parser.uri = req.from;
if (!from_parser.parse_uri()) {
WARN("Error parsing From URI '%s'\n", req.from.c_str());
break;
}
}
replaceParsedParam(s, p, from_parser, res);
}; break;
case 't': { // to
if ((s.length() == p+1) || (s[p+1] == '.')) {
res += req.to;
break;
}
if (to_parser.uri.empty()) {
to_parser.uri = req.to;
if (!to_parser.parse_uri()) {
WARN("Error parsing To URI '%s'\n", req.to.c_str());
break;
}
}
replaceParsedParam(s, p, to_parser, res);
}; break;
case 'r': { // r-uri
if ((s.length() == p+1) || (s[p+1] == '.')) {
res += req.r_uri;
break;
}
if (ruri_parser.uri.empty()) {
ruri_parser.uri = req.r_uri;
if (!ruri_parser.parse_uri()) {
WARN("Error parsing R-URI '%s'\n", req.r_uri.c_str());
break;
}
}
replaceParsedParam(s, p, ruri_parser, res);
}; break;
#define case_HDR(pv_char, pv_name, hdr_name) \
case pv_char: { \
AmUriParser uri_parser; \
uri_parser.uri = getHeader(req.hdrs, hdr_name); \
if ((s.length() == p+1) || (s[p+1] == '.')) { \
res += uri_parser.uri; \
break; \
} \
\
if (!uri_parser.parse_uri()) { \
WARN("Error parsing " pv_name " URI '%s'\n", uri_parser.uri.c_str()); \
break; \
} \
if (s[p+1] == 'i') { \
res+=uri_parser.uri_user+"@"+uri_parser.uri_host; \
if (!uri_parser.uri_port.empty()) \
res+=":"+uri_parser.uri_port; \
} else { \
replaceParsedParam(s, p, uri_parser, res); \
} \
}; break;
case_HDR('a', "PAI", SIP_HDR_P_ASSERTED_IDENTITY); // P-Asserted-Identity
case_HDR('p', "PPI", SIP_HDR_P_PREFERRED_IDENTITY); // P-Preferred-Identity
case 'P': { // app-params
if (s[p+1] != '(') {
WARN("Error parsing P param replacement (missing '(')\n");
break;
}
if (s.length()<p+3) {
WARN("Error parsing P param replacement (short string)\n");
break;
}
size_t skip_p = p+2;
for (;skip_p<s.length() && s[skip_p] != ')';skip_p++) { }
if (skip_p==s.length()) {
WARN("Error parsing P param replacement (unclosed brackets)\n");
break;
}
string param_name = s.substr(p+2, skip_p-p-2);
// DBG("param_name = '%s' (skip-p - p = %d)\n", param_name.c_str(), skip_p-p);
res += get_header_keyvalue(app_param, param_name);
skip_chars = skip_p-p;
} break;
default: {
WARN("unknown replace pattern $%c%c\n",
s[p], s[p+1]);
}; break;
};
p+=skip_chars; // skip $.X
} else {
res += s[p];
}
last_char = s[p];
p++;
}
if (is_replaced) {
DBG("%s pattern replace: '%s' -> '%s'\n", r_type, s.c_str(), res.c_str());
}
return res;
}
void SBCDialog::onInvite(const AmSipRequest& req)
{
AmUriParser ruri_parser, from_parser, to_parser;
DBG("processing initial INVITE\n");
if(dlg.reply(req, 100, "Connecting") != 0) {
throw AmSession::Exception(500,"Failed to reply 100");
}
string app_param = getHeader(req.hdrs, PARAM_HDR, true);
ruri = call_profile.ruri.empty() ?
req.r_uri : replaceParameters("RURI", call_profile.ruri, req, app_param,
ruri_parser, from_parser, to_parser);
from = call_profile.from.empty() ?
req.from : replaceParameters("From", call_profile.from, req, app_param,
ruri_parser, from_parser, to_parser);
to = call_profile.to.empty() ?
req.to : replaceParameters("To", call_profile.to, req, app_param,
ruri_parser, from_parser, to_parser);
m_state = BB_Dialing;
invite_req = req;
removeHeader(invite_req.hdrs,PARAM_HDR);
removeHeader(invite_req.hdrs,"P-App-Name");
if (call_profile.sst_enabled) {
removeHeader(invite_req.hdrs,SIP_HDR_SESSION_EXPIRES);
removeHeader(invite_req.hdrs,SIP_HDR_MIN_SE);
}
inplaceHeaderFilter(invite_req.hdrs,
call_profile.headerfilter_list, call_profile.headerfilter);
if (call_profile.auth_enabled) {
call_profile.auth_credentials.user =
replaceParameters("auth_user", call_profile.auth_credentials.user, req, app_param,
ruri_parser, from_parser, to_parser);
call_profile.auth_credentials.pwd =
replaceParameters("auth_pwd", call_profile.auth_credentials.pwd, req, app_param,
ruri_parser, from_parser, to_parser);
}
DBG("SBC: connecting to <%s>\n",ruri.c_str());
DBG(" From: <%s>\n",from.c_str());
DBG(" To: <%s>\n",to.c_str());
connectCallee(to, ruri, true);
}
void SBCDialog::process(AmEvent* ev)
{
AmB2BCallerSession::process(ev);
}
void SBCDialog::relayEvent(AmEvent* ev) {
if (call_profile.headerfilter != Transparent) {
if (ev->event_id == B2BSipRequest) {
B2BSipRequestEvent* req_ev = dynamic_cast<B2BSipRequestEvent*>(ev);
assert(req_ev);
inplaceHeaderFilter(req_ev->req.hdrs,
call_profile.headerfilter_list, call_profile.headerfilter);
} else if (ev->event_id == B2BSipReply) {
B2BSipReplyEvent* reply_ev = dynamic_cast<B2BSipReplyEvent*>(ev);
assert(reply_ev);
inplaceHeaderFilter(reply_ev->reply.hdrs,
call_profile.headerfilter_list, call_profile.headerfilter);
}
}
AmB2BCallerSession::relayEvent(ev);
}
void SBCDialog::onSipRequest(const AmSipRequest& req) {
// AmB2BSession does not call AmSession::onSipRequest for
// forwarded requests - so lets call event handlers here
// todo: this is a hack, replace this by calling proper session
// event handler in AmB2BSession
bool fwd = sip_relay_only &&
(req.method != "BYE") &&
(req.method != "CANCEL");
if (fwd) {
CALL_EVENT_H(onSipRequest,req);
}
if (fwd && call_profile.messagefilter != Transparent) {
bool is_filtered = (call_profile.messagefilter == Whitelist) ^
(call_profile.messagefilter_list.find(req.method) !=
call_profile.messagefilter_list.end());
if (is_filtered) {
DBG("replying 405 to filtered message '%s'\n", req.method.c_str());
dlg.reply(req, 405, "Method Not Allowed", "", "", "", SIP_FLAGS_VERBATIM);
return;
}
}
AmB2BCallerSession::onSipRequest(req);
}
void SBCDialog::onSipReply(const AmSipReply& reply, int old_dlg_status,
const string& trans_method)
{
TransMap::iterator t = relayed_req.find(reply.cseq);
bool fwd = t != relayed_req.end();
DBG("onSipReply: %i %s (fwd=%i)\n",reply.code,reply.reason.c_str(),fwd);
DBG("onSipReply: content-type = %s\n",reply.content_type.c_str());
if (fwd) {
CALL_EVENT_H(onSipReply,reply, old_dlg_status, trans_method);
}
AmB2BCallerSession::onSipReply(reply,old_dlg_status, trans_method);
}
bool SBCDialog::onOtherReply(const AmSipReply& reply)
{
bool ret = false;
if ((m_state == BB_Dialing) && (reply.cseq == invite_req.cseq)) {
if (reply.code < 200) {
DBG("Callee is trying... code %d\n", reply.code);
}
else if(reply.code < 300) {
if(getCalleeStatus() == Connected) {
m_state = BB_Connected;
}
}
else if(reply.code == 487 && dlg.getStatus() == AmSipDialog::Pending) {
DBG("Stopping leg A on 487 from B with 487\n");
dlg.reply(invite_req, 487, "Request terminated");
setStopped();
ret = true;
}
else if (reply.code >= 300 && dlg.getStatus() == AmSipDialog::Connected) {
DBG("Callee final error in connected state with code %d\n",reply.code);
terminateLeg();
}
else {
DBG("Callee final error with code %d\n",reply.code);
AmB2BCallerSession::onOtherReply(reply);
}
}
return ret;
}
void SBCDialog::onOtherBye(const AmSipRequest& req)
{
// stopAccounting();
AmB2BCallerSession::onOtherBye(req);
}
void SBCDialog::onBye(const AmSipRequest& req)
{
if (m_state == BB_Connected) {
// stopAccounting();
}
terminateOtherLeg();
setStopped();
}
void SBCDialog::onCancel()
{
if(dlg.getStatus() == AmSipDialog::Pending) {
DBG("Wait for leg B to terminate");
} else {
DBG("Canceling leg A on CANCEL since dialog is not pending");
dlg.reply(invite_req, 487, "Request terminated");
setStopped();
}
}
void SBCDialog::createCalleeSession()
{
SBCCalleeSession* callee_session = new SBCCalleeSession(this, call_profile);
if (call_profile.auth_enabled) {
// adding auth handler
AmSessionEventHandlerFactory* uac_auth_f =
AmPlugIn::instance()->getFactory4Seh("uac_auth");
if (NULL == uac_auth_f) {
INFO("uac_auth module not loaded. uac auth NOT enabled.\n");
} else {
AmSessionEventHandler* h = uac_auth_f->getHandler(callee_session);
// we cannot use the generic AmSessionEventHandler hooks,
// because the hooks don't work in AmB2BSession
callee_session->setAuthHandler(h);
DBG("uac auth enabled for callee session.\n");
}
}
if (call_profile.sst_enabled) {
AmSessionEventHandler* h = SBCFactory::session_timer_fact->getHandler(callee_session);
if(!h) {
ERROR("could not get a session timer event handler\n");
delete callee_session;
throw AmSession::Exception(500,"Server internal error");
}
AmConfigReader& sst_cfg = call_profile.use_global_sst_config ?
SBCFactory::cfg: call_profile.cfg; // override with profile config
if(h->configure(sst_cfg)){
ERROR("Could not configure the session timer: disabling session timers.\n");
delete h;
} else {
callee_session->addHandler(h);
}
}
AmSipDialog& callee_dlg = callee_session->dlg;
other_id = AmSession::getNewId();
callee_dlg.local_tag = other_id;
callee_dlg.callid = AmSession::getNewId() + "@" + AmConfig::LocalIP;
// this will be overwritten by ConnectLeg event
callee_dlg.remote_party = to;
callee_dlg.remote_uri = ruri;
callee_dlg.local_party = from;
callee_dlg.local_uri = from;
DBG("Created B2BUA callee leg, From: %s\n",
from.c_str());
if (AmConfig::LogSessions) {
INFO("Starting B2B callee session %s app %s\n",
callee_session->getLocalTag().c_str(), invite_req.cmd.c_str());
}
MONITORING_LOG5(other_id.c_str(),
"app", invite_req.cmd.c_str(),
"dir", "out",
"from", callee_dlg.local_party.c_str(),
"to", callee_dlg.remote_party.c_str(),
"ruri", callee_dlg.remote_uri.c_str());
callee_session->start();
AmSessionContainer* sess_cont = AmSessionContainer::instance();
sess_cont->addSession(other_id,callee_session);
}
SBCCalleeSession::SBCCalleeSession(const AmB2BCallerSession* caller,
const SBCCallProfile& call_profile)
: auth(NULL),
call_profile(call_profile),
AmB2BCalleeSession(caller) {
}
SBCCalleeSession::~SBCCalleeSession() {
if (auth)
delete auth;
}
inline UACAuthCred* SBCCalleeSession::getCredentials() {
return &call_profile.auth_credentials;
}
void SBCCalleeSession::relayEvent(AmEvent* ev) {
if (call_profile.headerfilter != Transparent) {
if (ev->event_id == B2BSipRequest) {
B2BSipRequestEvent* req_ev = dynamic_cast<B2BSipRequestEvent*>(ev);
assert(req_ev);
inplaceHeaderFilter(req_ev->req.hdrs,
call_profile.headerfilter_list, call_profile.headerfilter);
} else if (ev->event_id == B2BSipReply) {
B2BSipReplyEvent* reply_ev = dynamic_cast<B2BSipReplyEvent*>(ev);
assert(reply_ev);
inplaceHeaderFilter(reply_ev->reply.hdrs,
call_profile.headerfilter_list, call_profile.headerfilter);
}
}
AmB2BCalleeSession::relayEvent(ev);
}
void SBCCalleeSession::onSipRequest(const AmSipRequest& req) {
// AmB2BSession does not call AmSession::onSipRequest for
// forwarded requests - so lets call event handlers here
// todo: this is a hack, replace this by calling proper session
// event handler in AmB2BSession
bool fwd = sip_relay_only &&
(req.method != "BYE") &&
(req.method != "CANCEL");
if (fwd) {
CALL_EVENT_H(onSipRequest,req);
}
if (fwd && call_profile.messagefilter != Transparent) {
bool is_filtered = (call_profile.messagefilter == Whitelist) ^
(call_profile.messagefilter_list.find(req.method) !=
call_profile.messagefilter_list.end());
if (is_filtered) {
DBG("replying 405 to filtered message '%s'\n", req.method.c_str());
dlg.reply(req, 405, "Method Not Allowed", "", "", "", SIP_FLAGS_VERBATIM);
return;
}
}
AmB2BCalleeSession::onSipRequest(req);
}
void SBCCalleeSession::onSipReply(const AmSipReply& reply, int old_dlg_status,
const string& trans_method)
{
// call event handlers where it is not done
TransMap::iterator t = relayed_req.find(reply.cseq);
bool fwd = t != relayed_req.end();
DBG("onSipReply: %i %s (fwd=%i)\n",reply.code,reply.reason.c_str(),fwd);
DBG("onSipReply: content-type = %s\n",reply.content_type.c_str());
if(fwd) {
CALL_EVENT_H(onSipReply,reply, old_dlg_status, trans_method);
}
if (NULL == auth) {
AmB2BCalleeSession::onSipReply(reply,old_dlg_status, trans_method);
return;
}
unsigned int cseq_before = dlg.cseq;
if (!auth->onSipReply(reply, old_dlg_status, trans_method)) {
AmB2BCalleeSession::onSipReply(reply, old_dlg_status, trans_method);
} else {
if (cseq_before != dlg.cseq) {
DBG("uac_auth consumed reply with cseq %d and resent with cseq %d; "
"updating relayed_req map\n",
reply.cseq, cseq_before);
TransMap::iterator it=relayed_req.find(reply.cseq);
if (it != relayed_req.end()) {
relayed_req[cseq_before] = it->second;
relayed_req.erase(it);
}
}
}
}
void SBCCalleeSession::onSendRequest(const string& method, const string& content_type,
const string& body, string& hdrs, int flags, unsigned int cseq)
{
if (NULL != auth) {
DBG("auth->onSendRequest cseq = %d\n", cseq);
auth->onSendRequest(method, content_type,
body, hdrs, flags, cseq);
}
AmB2BCalleeSession::onSendRequest(method, content_type,
body, hdrs, flags, cseq);
}

@ -0,0 +1,174 @@
/*
* Copyright (C) 2010 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 _SBC_H
#define _SBC_H
#include "AmB2BSession.h"
#include "ampi/UACAuthAPI.h"
#include "AmConfigReader.h"
#include "AmUriParser.h"
#include "HeaderFilter.h"
#include <map>
using std::string;
struct SBCCallProfile {
AmConfigReader cfg;
FilterType headerfilter;
set<string> headerfilter_list;
FilterType messagefilter;
set<string> messagefilter_list;
bool sst_enabled;
bool use_global_sst_config;
bool auth_enabled;
UACAuthCred auth_credentials;
string ruri; /* updated if set */
string from; /* updated if set */
string to; /* updated if set */
// todo: call duration timer
// todo: accounting
// todo: RTP forwarding mode
// todo: RTP transcoding mode
SBCCallProfile()
: headerfilter(Transparent),
messagefilter(Transparent),
sst_enabled(false),
auth_enabled(false)
{ }
bool readFromConfiguration(const string& name, const string profile_file_name);
};
class SBCFactory: public AmSessionFactory
{
/* AmDynInvokeFactory* user_timer_fact; */
std::map<string, SBCCallProfile> call_profiles;
std::map<string, AmConfigReader> call_profiles_configs;
string active_profile;
public:
SBCFactory(const string& _app_name);
int onLoad();
AmSession* onInvite(const AmSipRequest& req);
static string user;
static string domain;
static string pwd;
static AmConfigReader cfg;
static AmSessionEventHandlerFactory* session_timer_fact;
};
class SBCDialog : public AmB2BCallerSession
{
enum {
BB_Init = 0,
BB_Dialing,
BB_Connected,
BB_Teardown
} CallerState;
int m_state;
string ruri;
string from;
string to;
/* AmDynInvoke* m_user_timer; */
SBCCallProfile call_profile;
void replaceParsedParam(const string& s, size_t p,
AmUriParser& parsed, string& res);
string replaceParameters(const char* r_type, const string& s, const AmSipRequest& req,
const string& app_param,
AmUriParser& ruri_parser, AmUriParser& from_parser,
AmUriParser& to_parser);
public:
SBCDialog(const SBCCallProfile& call_profile); //AmDynInvoke* user_timer);
~SBCDialog();
void process(AmEvent* ev);
void onBye(const AmSipRequest& req);
void onInvite(const AmSipRequest& req);
void onCancel();
protected:
void relayEvent(AmEvent* ev);
void onSipReply(const AmSipReply& reply, int old_dlg_status,
const string& trans_method);
void onSipRequest(const AmSipRequest& req);
bool onOtherReply(const AmSipReply& reply);
void onOtherBye(const AmSipRequest& req);
void createCalleeSession();
};
class SBCCalleeSession
: public AmB2BCalleeSession, public CredentialHolder
{
AmSessionEventHandler* auth;
SBCCallProfile call_profile;
protected:
void relayEvent(AmEvent* ev);
void onSipRequest(const AmSipRequest& req);
void onSipReply(const AmSipReply& reply, int old_dlg_status,
const string& trans_method);
void onSendRequest(const string& method, const string& content_type,
const string& body, string& hdrs, int flags, unsigned int cseq);
/* bool onOtherReply(const AmSipReply& reply); */
public:
SBCCalleeSession(const AmB2BCallerSession* caller,
const SBCCallProfile& call_profile);
~SBCCalleeSession();
inline UACAuthCred* getCredentials();
void setAuthHandler(AmSessionEventHandler* h) { auth = h; }
};
#endif

@ -0,0 +1,76 @@
# auth_b2b SBC profile
#
# This implements the identity change and SIP
# authentication known from auth_b2b app.
# For a more detailed description see the
# explanation below.
#
# A P-App-Param header is expected of the form:
# P-App-Param: u=<user>;d=<domain>;p=<pwd>
# The INVITE is then sent from <user>@<domain>
# to ruri-user@<domain>
#
# if the user/domain/password should be set here
# in the configuration, replace $P(u), $P(p) and $P(d)
# below.
RURI=sip:$rU@$P(d)
From="\"$P(u)\" <sip:$P(u)@$P(d)>"
To="\"$rU\" <sip:$rU@$P(d)>"
enable_auth=yes
auth_user=$P(u)
auth_pwd=$P(p)
header_filter=blacklist
header_list=P-App-Param,P-App-Name
message_filter=transparent
#message_list=
# set this for session timer:
#enable_session_timer=yes
#session_expires=120
#minimum_timer=90
#session_refresh_method=UPDATE_FALLBACK_INVITE
#accept_501_reply=yes
#This profile implements a pure B2BUA application that does an
#identity change and authenticates on the second leg of the call,
#like this
#
#Caller SEMS auth_b2b 123@domainb
# | | |
# | INVITE bob@domaina | |
# | From: alice@domaina | |
# | To: bob@domaina | |
# | P-App-Param:u=user;d=domainb;p=passwd |
# |-------------------->| |
# | |INVITE bob@domainb |
# | |From: user@domainb |
# | |To: bob@domainb |
# | |----------------------->|
# | | |
# | | 407 auth required |
# | |<---------------------- |
# | | |
# | | |
# | | INVITE w/ auth |
# | |----------------------->|
# | | |
# | | 100 trying |
# | 100 trying |<---------------------- |
# |<--------------------| |
# | | |
# | | 200 OK |
# | 200 OK |<---------------------- |
# |<--------------------| |
# | | |
# | ACK | |
# |-------------------->| ACK |
# | |----------------------->|
#
#App-Param:
# u - user in B leg
# d - domain in B leg
# p - password for auth in B leg (auth user=user)

@ -0,0 +1,51 @@
# profiles - comma-separated list of call profiles to load
#
# <name>.sbcprofile.conf is loaded from module config
# path (the path where this file resides)
profiles=transparent,auth_b2b,sst_b2b
# active call profile
#
# o active_profile=<profile_name> always use <profile_name>
#
# o active_profile=$(ruri.user) use user part of INVITE Request URI
#
# o active_profile=$(paramhdr) use "profile" option in P-App-Param header
#
active_profile=transparent
## RFC4028 Session Timer
# default configuration - can be overridden by call profiles
# - enables the session timer ([yes,no]; default: no)
#
#enable_session_timer=yes
# - set the "Session-Expires" parameter for the session timer.
#
# session_expires=240
# - set the "Min-SE" parameter for the session timer.
#
# minimum_timer=90
# session refresh (Session Timer, RFC4028) method
#
# INVITE - use re-INVITE
# UPDATE - use UPDATE
# UPDATE_FALLBACK_INVITE - use UPDATE if indicated in Allow, re-INVITE otherwise
#
# 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]
#
# Default: yes
#
#accept_501_reply=no

@ -0,0 +1,39 @@
# RFC4028 Session Timer
#
# - enables the session timer ([yes,no]; default: no)
#
enable_session_timer=yes
# - set the "Session-Expires" parameter for the session timer.
#
# session_expires=240
# - set the "Min-SE" parameter for the session timer.
#
# minimum_timer=90
# session refresh (Session Timer, RFC4028) method
#
# INVITE - use re-INVITE
# UPDATE - use UPDATE
# UPDATE_FALLBACK_INVITE - use UPDATE if indicated in Allow, re-INVITE otherwise
#
# 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]
#
# Default: yes
#
#accept_501_reply=no
######################################################
#authentication (questionable whether that works)
# user=someuser
# domain=somedomain.net
# pwd=sompwd

@ -0,0 +1,53 @@
# sst_b2b SBC profile
#
# This implements a transparent B2BUA with
# SIP Session Timers known from the sst_b2b app.
# Also see the description below.
# defaults: transparent
#RURI=$r
#From=$f
#To=$t
## filters:
#header_filter=blacklist
#header_list=P-App-Param,P-App-Name
#message_filter=transparent
#message_list=
## authentication:
#enable_auth=yes
#auth_user=$P(u)
#auth_pwd=$P(p)
## session timer:
enable_session_timer=yes
# if session_expires is not configured here,
# the values from sbc.conf are used, or the
# default values
#session_expires=120
#minimum_timer=90
#session_refresh_method=UPDATE_FALLBACK_INVITE
#accept_501_reply=yes
#
#This application can be routed through for achieving
#two things:
#
# 1. Forcing SIP Session Timers, which prevents
# overbilling for calls where BYE is missing,
# for example in cases where media (RTP) path
# does not go through the system, but billing
# is still done.
#
# 2. Topology hiding; this application acts as
# B2BUA, so on the B leg, no routing info from
# the A leg can be seen.
#
#The incoming INVITE for a newly established call is
#passed in signaling only B2B mode to the B leg,
#which tries to send it to the request URI.
#
#SIP Session Timers are enabled on both legs. The
#session refresh method may be configured; UPDATE
#or INVITE with last established SDP may be used.

@ -0,0 +1,29 @@
# transparent SBC profile
#
# This implements a transparent B2BUA
# defaults: transparent
#RURI=$r
#From=$f
#To=$t
## filters:
#header_filter=blacklist
#header_list=P-App-Param,P-App-Name
#message_filter=transparent
#message_list=
## authentication:
#enable_auth=yes
#auth_user=$P(u)
#auth_pwd=$P(p)
## session timer:
#enable_session_timer=yes
# if session_expires is not configured here,
# the values from sbc.conf are used, or the
# default values
#session_expires=120
#minimum_timer=90
#session_refresh_method=UPDATE_FALLBACK_INVITE
#accept_501_reply=yes

@ -1,6 +1,14 @@
auth_b2b application
-------------------------------------------------
This application has been obsoleted by the sbc
module and will be discontinued in the next version.
Please use the sbc module with the auth_b2b call
profile for the same functionality.
-------------------------------------------------
This module is a pure B2BUA application that does an identity
change and authenticates on the second leg of the call, like
this

@ -0,0 +1,157 @@
SBC module
Copyright (C) 2010 Stefan Sayer
Overview
--------
The SBC application is a highly flexible high-performance Back-to-Back
User Agent (B2BUA). It can be employed for a variety of uses, for example
topology hiding, From/To modification, enforcing SIP Session Timers,
identity change, SIP authentication. Future uses include accounting,
call timers, RTP call bridging, transcoding, call distribution.
Features
--------
o B2BUA
o flexible call profile based configuration
o From, To, RURI update
o Header and message filter
o SIP authentication
o SIP Session Timers
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
o statically (active_profile=<profile_name>)
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.
RURI, From, To - Replacement patterns
-------------------------------------
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.
The values that are set can contain patterns, which are set to values taken
from the incoming INVITE request. The syntax loosely follows sip-router's
pseudo variables. Any of the RURI, From and To values can contain any elements,
e.g. the request-URI can be set to the user part of the P-Asserted-Identity
header combined with the host part of the To.
The patterns which can be used are the following:
$r (or $r. if something follows) - R-URI
$f (or $f. if something follows) - From
$t (or $t. if something follows) - To
$a (or $a. if something follows) - P-Asserted-Identity
$p (or $p. if something follows) - P-Preferref-Identity
$fu - From URI
$fU - From User
$fd - From domain (host:port)
$fh - From host
$fp - From port
$fH - From headers
$fP - From Params
$tu - To URI
$fU - To User
...
$ru - R-URI URI
$rU - R-URI User
...
$ai - P-Asserted-Identity URI
$au - P-Asserted-Identity URI
$aU - P-Asserted-Identity URI
...
$pi - P-Preferred-Identity URI
$pu - P-Preferred-Identity URI
$pU - P-Preferred-Identity User
...
$P(paramname) - paramname from P-App-Param
Example:
P-App-Param: u=myuser;p=mypwd;d=mydomain
and
auth_user=$P(u)
auth_pwd=$P(p)
From=sip:$P(u)@$P(d)
\\ -> \
\$ -> $
\* -> *
If a quotation mark (") is used, it needs to be escaped with a backslash in
the sbc profile configuration file.
Example:
From="\"Anonymous\" <sip:anonymous@invalid>"
If a space is contained, use quotation at the beginning and end.
Example:
To="\"someone\" <$aU@mytodomain.com>"
Filters
-------
Headers and messages may be filtered. A filter can be set to
o transparent - no filtering done
o whitelist - only let items pass that are in the filter list
o blacklist - filter out items that are in the filter list
Note that if ACK messages should not be filtered.
Session Timer configuration
---------------------------
If SIP Session Timers are enabled for a profile, the session timers values
(session_refresh, minimum_timer etc) can be configured either in sbc.conf
or in the profile configuration. The profile SST configuration is used if
session_expires is set in the profile configuration file.
Note that for performance reasons the whole SST configuration is in this
case used from the profile configuration (it is not overwritten value-by-value).
Example profiles
----------------
transparent - completely transparent B2BUA
auth_b2b - identity change and SIP authentication (obsoletes auth_b2b app)
sst_b2b - B2BUA with SIP Session Timers (obsoletes sst_b2b app)
Dependencies
------------
For SIP authentication: uac_auth module
For SIP Session Timers and call timers: session_timer module
Roadmap
-------
x header filter (whitelist or blacklist)
x message filter (whitelist or blacklist)
- SDP filter (reconstructed SDP)
x remote URI update (host / user / host/user)
x From update (displayname / host / host/user)
x To update (displayname / host / host/user)
x SIP authentication
x session timers
- maximum call duration timer
- accounting (MySQL DB, cassandra DB)
- RTP forwarding mode (bridging)
- RTP transcoding mode (bridging)
- overload handling (parallel call to target thresholds)
- call distribution
- select profile on monitoring in-mem DB record
- fallback profile
- add headers
- bridging between interfaces

@ -1,5 +1,12 @@
SIP Session Timers (SST) enabled B2B application.
-------------------------------------------------
This application has been obsoleted by the sbc
module and will be discontinued in the next version.
Please use the sbc module with the sst_b2b call
profile for the same functionality.
-------------------------------------------------
This application can be routed through for achieving
two things:
@ -17,15 +24,13 @@ The incoming INVITE for a newly established call is
passed in signaling only B2B mode to the B leg,
which tries to send it to the request URI.
SIP Session Timers are enabled on both legs. When the
timer expires, an empty INVITE is sent, and the resulting
SDP offer from body of the 200 is relayed into the other
leg, where it is sent out as INVITE with the offer. The
answer from B leg is relayed into A leg and sent as body
in ACK message.
SIP Session Timers are enabled on both legs. The
session refresh method may be configured; UPDATE
or INVITE with last established SDP may be used.
SST expiration is configurable in config file.
Session refresh with last established SDP:
A b2b B
|---INVITE / SDPa-->| |
@ -39,9 +44,11 @@ SST expiration is configurable in config file.
... SST timer expires :
| | |
|<-- INVITE --------| |
|- OK/SDPc (offer)->| |
| |---INVITE / SDPc-->|
| |<-- OK/SDPd (answ)-|
|<----ACK/SDPd------|----ACK----------->|
|<-- INVITE / SDPb -| |
|- OK/SDPa (offer)->| |
|<----ACK ---------| |
| | |
| |---INVITE / SDPa-->|
| |<-- OK/SDPb (answ)-|
| |----ACK----------->|

Loading…
Cancel
Save