diff --git a/apps/sbc/CMakeLists.txt b/apps/sbc/CMakeLists.txt new file mode 100644 index 00000000..87f87671 --- /dev/null +++ b/apps/sbc/CMakeLists.txt @@ -0,0 +1,6 @@ +set (sbc_SRCS +SBC.cpp +) + +SET(sems_module_name sbc) +INCLUDE(${CMAKE_SOURCE_DIR}/cmake/module.rules.txt) diff --git a/apps/sbc/HeaderFilter.cpp b/apps/sbc/HeaderFilter.cpp new file mode 100644 index 00000000..a12a92e4 --- /dev/null +++ b/apps/sbc/HeaderFilter.cpp @@ -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& headerfilter_list, FilterType f_type) { + if (!hdrs.length() || f_type == Transparent) + return 0; + + int res = 0; + size_t start_pos = 0; + while (start_pos +using std::string; + +#include +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& headerfilter_list, + FilterType f_type); + +#endif diff --git a/apps/sbc/Makefile b/apps/sbc/Makefile new file mode 100644 index 00000000..90f33a52 --- /dev/null +++ b/apps/sbc/Makefile @@ -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 diff --git a/apps/sbc/SBC.cpp b/apps/sbc/SBC.cpp new file mode 100644 index 00000000..3534e954 --- /dev/null +++ b/apps/sbc/SBC.cpp @@ -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 elems = explode(cfg.getParameter("header_list"), ","); + for (vector::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::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 profiles_names = explode(cfg.getParameter("profiles"), ","); + for (vector::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::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= 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() '%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(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(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(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(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); +} + diff --git a/apps/sbc/SBC.h b/apps/sbc/SBC.h new file mode 100644 index 00000000..95d76352 --- /dev/null +++ b/apps/sbc/SBC.h @@ -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 + +using std::string; + + +struct SBCCallProfile { + + AmConfigReader cfg; + + FilterType headerfilter; + set headerfilter_list; + + FilterType messagefilter; + set 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 call_profiles; + std::map 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 diff --git a/apps/sbc/etc/auth_b2b.sbcprofile.conf b/apps/sbc/etc/auth_b2b.sbcprofile.conf new file mode 100644 index 00000000..79c0e519 --- /dev/null +++ b/apps/sbc/etc/auth_b2b.sbcprofile.conf @@ -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=;d=;p= +# The INVITE is then sent from @ +# to ruri-user@ +# +# 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)\" " +To="\"$rU\" " + +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) diff --git a/apps/sbc/etc/sbc.conf b/apps/sbc/etc/sbc.conf new file mode 100644 index 00000000..50b1574b --- /dev/null +++ b/apps/sbc/etc/sbc.conf @@ -0,0 +1,51 @@ + +# profiles - comma-separated list of call profiles to load +# +# .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= always use +# +# 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 + diff --git a/apps/sbc/etc/sbc.conf.cmake b/apps/sbc/etc/sbc.conf.cmake new file mode 100644 index 00000000..a939a15d --- /dev/null +++ b/apps/sbc/etc/sbc.conf.cmake @@ -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 + diff --git a/apps/sbc/etc/sst_b2b.sbcprofile.conf b/apps/sbc/etc/sst_b2b.sbcprofile.conf new file mode 100644 index 00000000..9deb0445 --- /dev/null +++ b/apps/sbc/etc/sst_b2b.sbcprofile.conf @@ -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. diff --git a/apps/sbc/etc/transparent.sbcprofile.conf b/apps/sbc/etc/transparent.sbcprofile.conf new file mode 100644 index 00000000..aa5a8a69 --- /dev/null +++ b/apps/sbc/etc/transparent.sbcprofile.conf @@ -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 \ No newline at end of file diff --git a/doc/Readme.auth_b2b.txt b/doc/Readme.auth_b2b.txt index aad91d15..4c3174c8 100644 --- a/doc/Readme.auth_b2b.txt +++ b/doc/Readme.auth_b2b.txt @@ -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 diff --git a/doc/Readme.sbc.txt b/doc/Readme.sbc.txt new file mode 100644 index 00000000..0a1121f0 --- /dev/null +++ b/doc/Readme.sbc.txt @@ -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=) + + 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\" " + +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 diff --git a/doc/Readme.sst_b2b.txt b/doc/Readme.sst_b2b.txt index 62310d8a..c41a1b36 100644 --- a/doc/Readme.sst_b2b.txt +++ b/doc/Readme.sst_b2b.txt @@ -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----------->|