You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sems/core/AmSipDialog.cpp

863 lines
21 KiB

/*
* Copyright (C) 2002-2003 Fhg Fokus
*
* 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. This program is released under
* the GPL with the additional exemption that compiling, linking,
* and/or using OpenSSL is allowed.
*
* 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 "AmSipDialog.h"
#include "AmConfig.h"
#include "AmSession.h"
#include "AmUtils.h"
#include "AmSipHeaders.h"
#include "SipCtrlInterface.h"
#include "sems.h"
#include "sip/parse_route.h"
#include "sip/parse_uri.h"
#include "sip/parse_next_hop.h"
#include "AmB2BMedia.h" // just because of statistics
static void addTranscoderStats(string &hdrs)
{
// add transcoder statistics into request/reply headers
if (!AmConfig::TranscoderOutStatsHdr.empty()) {
string usage;
B2BMediaStatistics::instance()->reportCodecWriteUsage(usage);
hdrs += AmConfig::TranscoderOutStatsHdr + ": ";
hdrs += usage;
hdrs += CRLF;
}
if (!AmConfig::TranscoderInStatsHdr.empty()) {
string usage;
B2BMediaStatistics::instance()->reportCodecReadUsage(usage);
hdrs += AmConfig::TranscoderInStatsHdr + ": ";
hdrs += usage;
hdrs += CRLF;
}
}
AmSipDialog::AmSipDialog(AmSipDialogEventHandler* h)
: AmBasicSipDialog(h),oa(this),rel100(this,h),
offeranswer_enabled(true),
early_session_started(false),session_started(false),
pending_invites(0),
sdp_local(), sdp_remote(), force_early_announce(false)
{
}
AmSipDialog::~AmSipDialog()
{
}
bool AmSipDialog::onRxReqSanity(const AmSipRequest& req)
{
if (req.method == SIP_METH_ACK) {
if(onRxReqStatus(req) && hdl)
hdl->onSipRequest(req);
return false;
}
if (req.method == SIP_METH_CANCEL) {
if (uas_trans.find(req.cseq) == uas_trans.end()) {
reply_error(req,481,SIP_REPLY_NOT_EXIST);
return false;
}
if(onRxReqStatus(req) && hdl)
hdl->onSipRequest(req);
return false;
}
if(!AmBasicSipDialog::onRxReqSanity(req))
return false;
if (req.method == SIP_METH_INVITE) {
bool pending = pending_invites;
if (offeranswer_enabled) {
// not sure this is needed here: could be in AmOfferAnswer as well
pending |= ((oa.getState() != AmOfferAnswer::OA_None) &&
(oa.getState() != AmOfferAnswer::OA_Completed));
}
if (pending) {
reply_error(req, 491, SIP_REPLY_PENDING,
SIP_HDR_COLSP(SIP_HDR_RETRY_AFTER)
+ int2str(get_random() % 10) + CRLF);
return false;
}
pending_invites++;
}
return rel100.onRequestIn(req);
}
bool AmSipDialog::onRxReqStatus(const AmSipRequest& req)
{
switch(status){
case Disconnected:
if(req.method == SIP_METH_INVITE)
setStatus(Trying);
break;
case Connected:
if(req.method == SIP_METH_BYE)
setStatus(Disconnecting);
break;
case Trying:
case Proceeding:
case Early:
if(req.method == SIP_METH_BYE)
setStatus(Disconnecting);
else if(req.method == SIP_METH_CANCEL){
setStatus(Cancelling);
reply(req,200,"OK");
}
break;
default: break;
}
bool cont = true;
if (offeranswer_enabled) {
cont = (oa.onRequestIn(req) == 0);
}
return cont;
}
int AmSipDialog::onSdpCompleted()
{
if(!hdl) return 0;
int ret = ((AmSipDialogEventHandler*)hdl)->
onSdpCompleted(oa.getLocalSdp(), oa.getRemoteSdp());
if(!ret) {
sdp_local = oa.getLocalSdp();
sdp_remote = oa.getRemoteSdp();
if((getStatus() == Early) && !early_session_started) {
((AmSipDialogEventHandler*)hdl)->onEarlySessionStart();
early_session_started = true;
}
if((getStatus() == Connected) && !session_started) {
((AmSipDialogEventHandler*)hdl)->onSessionStart();
session_started = true;
}
}
else {
oa.clear();
}
return ret;
}
bool AmSipDialog::getSdpOffer(AmSdp& offer)
{
if(!hdl) return false;
return ((AmSipDialogEventHandler*)hdl)->getSdpOffer(offer);
}
bool AmSipDialog::getSdpAnswer(const AmSdp& offer, AmSdp& answer)
{
if(!hdl) return false;
return ((AmSipDialogEventHandler*)hdl)->getSdpAnswer(offer,answer);
}
AmOfferAnswer::OAState AmSipDialog::getOAState() {
return oa.getState();
}
void AmSipDialog::setOAState(AmOfferAnswer::OAState n_st) {
oa.setState(n_st);
}
void AmSipDialog::setRel100State(Am100rel::State rel100_state) {
DBG("setting 100rel state for '%s' to %i\n", local_tag.c_str(), rel100_state);
rel100.setState(rel100_state);
}
void AmSipDialog::setOAEnabled(bool oa_enabled) {
DBG("%sabling offer_answer on SIP dialog '%s'\n",
oa_enabled?"en":"dis", local_tag.c_str());
offeranswer_enabled = oa_enabled;
}
int AmSipDialog::onTxRequest(AmSipRequest& req, int& flags)
{
rel100.onRequestOut(req);
if (offeranswer_enabled && oa.onRequestOut(req))
return -1;
if(AmBasicSipDialog::onTxRequest(req,flags) < 0)
return -1;
// add transcoder statistics into request headers
addTranscoderStats(req.hdrs);
if((req.method == SIP_METH_INVITE) && (status == Disconnected)){
setStatus(Trying);
}
else if((req.method == SIP_METH_BYE) && (status != Disconnecting)){
setStatus(Disconnecting);
}
if ((req.method == SIP_METH_BYE) || (req.method == SIP_METH_CANCEL)) {
flags |= SIP_FLAGS_NOCONTACT;
}
return 0;
}
// UAS behavior for locally sent replies
int AmSipDialog::onTxReply(const AmSipRequest& req, AmSipReply& reply, int& flags)
{
if (offeranswer_enabled) {
if(oa.onReplyOut(reply) < 0)
return -1;
}
rel100.onReplyOut(reply);
// update Dialog status
switch(status){
case Connected:
case Disconnected:
break;
case Cancelling:
if( (reply.cseq_method == SIP_METH_INVITE) &&
(reply.code < 200) ) {
// refuse local provisional replies
// when state is Cancelling
ERROR("refuse local provisional replies when state is Cancelling\n");
return -1;
}
// else continue with final
// reply processing
case Proceeding:
case Trying:
case Early:
if(reply.cseq_method == SIP_METH_INVITE){
if(reply.code < 200) {
setStatus(Early);
}
else if(reply.code < 300)
setStatus(Connected);
else
setStatus(Disconnected);
}
break;
case Disconnecting:
if(reply.cseq_method == SIP_METH_BYE){
// Only reason for refusing a BYE:
// authentication (NYI at this place)
// Also: we should not send provisionnal replies to a BYE
if(reply.code >= 200)
setStatus(Disconnected);
}
break;
default:
assert(0);
break;
}
// add transcoder statistics into reply headers
addTranscoderStats(reply.hdrs);
// target-refresh requests and their replies need to contain Contact (1xx
// replies only those establishing dialog, take care about them?)
if(reply.cseq_method != SIP_METH_INVITE &&
reply.cseq_method != SIP_METH_UPDATE) {
flags |= SIP_FLAGS_NOCONTACT;
}
return AmBasicSipDialog::onTxReply(req,reply,flags);
}
void AmSipDialog::onReplyTxed(const AmSipRequest& req, const AmSipReply& reply)
{
AmBasicSipDialog::onReplyTxed(req,reply);
if (offeranswer_enabled) {
oa.onReplySent(reply);
}
if (reply.code >= 200) {
if(reply.cseq_method == SIP_METH_INVITE)
pending_invites--;
}
}
void AmSipDialog::onRequestTxed(const AmSipRequest& req)
{
AmBasicSipDialog::onRequestTxed(req);
if (offeranswer_enabled) {
oa.onRequestSent(req);
}
}
bool AmSipDialog::onRxReplySanity(const AmSipReply& reply)
{
if(!getRemoteTag().empty()
&& reply.to_tag != getRemoteTag()) {
if(status == Early) {
if(reply.code < 200 && !reply.to_tag.empty()) {
return false;// DROP
}
}
else {
// DROP
return false;
}
}
return true;
}
bool AmSipDialog::onRxReplyStatus(const AmSipReply& reply)
{
/* rfc3261 12.1
Dialog established only by 101-199 or 2xx
responses to INVITE */
DBG("onRxReplyStatus: reply.code = <%d>, reply.route = <%s>, status = <%d>\n",
reply.code, reply.route.c_str(), status);
if (reply.cseq_method == SIP_METH_INVITE) {
switch (status) {
case Trying:
case Proceeding:
DBG("This is the Proceeding stage of the dialog.\n");
/* 100-199 */
if (reply.code < 200) {
if (reply.code == 100 || reply.to_tag.empty()) {
setStatus(Proceeding);
} else {
setStatus(Early);
setRemoteTag(reply.to_tag);
setRouteSet(reply.route);
}
/* 200-299 */
} else if(reply.code < 300) {
setStatus(Connected);
setRouteSet(reply.route);
if (reply.to_tag.empty()){
DBG("received 2xx reply without to-tag (callid=%s): sending BYE\n",
reply.callid.c_str());
send_200_ack(reply.cseq);
sendRequest(SIP_METH_BYE);
} else {
setRemoteTag(reply.to_tag);
}
/* 300-699 */
} else {
setStatus(Disconnected);
setRemoteTag(reply.to_tag);
}
break;
case Early:
DBG("This is the Early stage of the dialog.\n");
/* 100-199 */
if (reply.code < 200) {
DBG("ignoring provisional reply in Early state");
/* we should always keep Route set for this leg updated in case
the provisional response updates the list of routes for any reason */
if ((reply.code == 180 || reply.code == 183) && !reply.route.empty()) {
DBG("<%d> Response code in processed, reset the Route set for the leg.\n",
reply.code);
setRouteSet(reply.route);
}
/* 200-299 */
} else if(reply.code < 300) {
setStatus(Connected);
setRouteSet(reply.route);
if (reply.to_tag.empty()) {
DBG("received 2xx reply without to-tag (callid=%s): sending BYE\n",
reply.callid.c_str());
sendRequest(SIP_METH_BYE);
} else {
setRemoteTag(reply.to_tag);
}
/* 300-699 */
} else {
setStatus(Disconnected);
setRemoteTag(reply.to_tag);
}
break;
case Cancelling:
if (reply.code >= 300) { /* CANCEL accepted */
DBG("CANCEL accepted, status -> Disconnected\n");
setStatus(Disconnected);
} else if(reply.code < 300) { /* CANCEL rejected */
DBG("CANCEL rejected/too late - bye()\n");
setRemoteTag(reply.to_tag);
setStatus(Connected);
bye();
/* if for any reason BYE could not be sent,
there is nothing we can do anymore */
}
break;
/* case Connected: // late 200...
TODO: if reply.to_tag != getRemoteTag()
-> ACK + BYE (+absorb answer) */
default:
break;
}
}
if (status == Disconnecting) {
DBG("?Disconnecting?: cseq_method = %s; code = %i\n",
reply.cseq_method.c_str(), reply.code);
if ((reply.cseq_method == SIP_METH_BYE) && (reply.code >= 200)) {
/* TODO: support the auth case here (401/403) */
setStatus(Disconnected);
}
}
if (offeranswer_enabled) {
oa.onReplyIn(reply);
}
bool cont = true;
if ( (reply.code >= 200) && (reply.code < 300) &&
(reply.cseq_method == SIP_METH_INVITE) ) {
if(hdl) ((AmSipDialogEventHandler*)hdl)->onInvite2xx(reply);
} else {
cont = AmBasicSipDialog::onRxReplyStatus(reply);
}
return cont && rel100.onReplyIn(reply);
}
void AmSipDialog::uasTimeout(AmSipTimeoutEvent* to_ev)
{
assert(to_ev);
switch(to_ev->type){
case AmSipTimeoutEvent::noACK:
DBG("Timeout: missing ACK\n");
if (offeranswer_enabled) {
oa.onNoAck(to_ev->cseq);
}
if(hdl) ((AmSipDialogEventHandler*)hdl)->onNoAck(to_ev->cseq);
break;
case AmSipTimeoutEvent::noPRACK:
DBG("Timeout: missing PRACK\n");
rel100.onTimeout(to_ev->req, to_ev->rpl);
break;
case AmSipTimeoutEvent::_noEv:
default:
break;
};
to_ev->processed = true;
}
bool AmSipDialog::getUACInvTransPending() {
for (TransMap::iterator it=uac_trans.begin();
it != uac_trans.end(); it++) {
if (it->second.method == SIP_METH_INVITE)
return true;
}
return false;
}
AmSipRequest* AmSipDialog::getUASPendingInv()
{
for (TransMap::iterator it=uas_trans.begin();
it != uas_trans.end(); it++) {
if (it->second.method == SIP_METH_INVITE)
return &(it->second);
}
return NULL;
}
int AmSipDialog::bye(const string& hdrs, int flags)
{
switch(status){
case Disconnecting:
case Connected: {
// collect INVITE UAC transactions
vector<unsigned int> ack_trans;
for (TransMap::iterator it=uac_trans.begin(); it != uac_trans.end(); it++) {
if (it->second.method == SIP_METH_INVITE){
ack_trans.push_back(it->second.cseq);
}
}
// finish any UAC transaction before sending BYE
for (vector<unsigned int>::iterator it=
ack_trans.begin(); it != ack_trans.end(); it++) {
send_200_ack(*it);
}
if (status != Disconnecting) {
setStatus(Disconnected);
return sendRequest(SIP_METH_BYE, NULL, hdrs, flags);
} else {
return 0;
}
}
case Trying:
case Proceeding:
case Early:
if(getUACInvTransPending())
return cancel();
else {
for (TransMap::iterator it=uas_trans.begin();
it != uas_trans.end(); it++) {
if (it->second.method == SIP_METH_INVITE){
// let quit this call by sending final reply
return reply(it->second,
487,"Request terminated");
}
}
// missing AmSipRequest to be able
// to send the reply on behalf of the app.
ERROR("ignoring bye() in %s state: "
"no UAC transaction to cancel or UAS transaction to reply.\n",
getStatusStr());
setStatus(Disconnected);
}
return 0;
case Cancelling:
for (TransMap::iterator it=uas_trans.begin();
it != uas_trans.end(); it++) {
if (it->second.method == SIP_METH_INVITE){
// let's quit this call by sending final reply
return reply(it->second, 487,"Request terminated");
}
}
// missing AmSipRequest to be able
// to send the reply on behalf of the app.
DBG("ignoring bye() in %s state: no UAS transaction to reply",getStatusStr());
setStatus(Disconnected);
return 0;
default:
DBG("bye(): we are not connected "
"(status=%s). do nothing!\n",getStatusStr());
return 0;
}
}
int AmSipDialog::reinvite(const string& hdrs,
const AmMimeBody* body,
int flags)
{
if(getStatus() == Connected) {
return sendRequest(SIP_METH_INVITE, body, hdrs, flags);
}
else {
DBG("reinvite(): we are not connected "
"(status=%s). do nothing!\n",getStatusStr());
}
return -1;
}
int AmSipDialog::invite(const string& hdrs,
const AmMimeBody* body)
{
if(getStatus() == Disconnected) {
int res = sendRequest(SIP_METH_INVITE, body, hdrs);
DBG("TODO: is status already 'trying'? status=%s\n",getStatusStr());
//status = Trying;
return res;
}
else {
DBG("invite(): we are already connected "
"(status=%s). do nothing!\n",getStatusStr());
}
return -1;
}
int AmSipDialog::update(const AmMimeBody* body,
const string &hdrs)
{
switch(getStatus()){
case Connected://if Connected, we should send a re-INVITE instead...
DBG("re-INVITE should be used instead (see RFC3311, section 5.1)\n");
case Trying:
case Proceeding:
case Early:
return sendRequest(SIP_METH_UPDATE, body, hdrs);
default:
case Cancelling:
case Disconnected:
case Disconnecting:
DBG("update(): dialog not connected "
"(status=%s). do nothing!\n",getStatusStr());
}
return -1;
}
int AmSipDialog::refer(const string& refer_to,
int expires,
const string& referred_by)
{
if(getStatus() == Connected) {
string hdrs = SIP_HDR_COLSP(SIP_HDR_REFER_TO) + refer_to + CRLF;
if (expires>=0)
hdrs+= SIP_HDR_COLSP(SIP_HDR_EXPIRES) + int2str(expires) + CRLF;
if (!referred_by.empty())
hdrs+= SIP_HDR_COLSP(SIP_HDR_REFERRED_BY) + referred_by + CRLF;
return sendRequest("REFER", NULL, hdrs);
}
else {
DBG("refer(): we are not Connected."
"(status=%s). do nothing!\n",getStatusStr());
return 0;
}
}
int AmSipDialog::info(const string& hdrs, const AmMimeBody* body)
{
if(getStatus() == Connected) {
return sendRequest("INFO", body, hdrs);
} else {
DBG("info(): we are not Connected."
"(status=%s). do nothing!\n", getStatusStr());
return 0;
}
}
// proprietary
int AmSipDialog::transfer(const string& target)
{
if(getStatus() == Connected){
setStatus(Disconnecting);
string hdrs = "";
AmSipDialog tmp_d(*this);
tmp_d.route = "";
// TODO!!!
//tmp_d.contact_uri = SIP_HDR_COLSP(SIP_HDR_CONTACT)
// "<" + tmp_d.remote_uri + ">" CRLF;
tmp_d.remote_uri = target;
string r_set;
if(!route.empty()){
hdrs = PARAM_HDR ": " "Transfer-RR=\"" + route + "\""+CRLF;
}
int ret = tmp_d.sendRequest("REFER",NULL,hdrs);
if(!ret){
uac_trans.insert(tmp_d.uac_trans.begin(),
tmp_d.uac_trans.end());
cseq = tmp_d.cseq;
}
return ret;
}
DBG("transfer(): we are not connected "
"(status=%i). do nothing!\n",status);
return 0;
}
int AmSipDialog::prack(const AmSipReply &reply1xx,
const AmMimeBody* body,
const string &hdrs)
{
switch(getStatus()) {
case Trying:
case Proceeding:
case Cancelling:
case Early:
case Connected:
break;
case Disconnected:
case Disconnecting:
ERROR("can not send PRACK while dialog is in state '%d'.\n", status);
return -1;
default:
ERROR("BUG: unexpected dialog state '%d'.\n", status);
return -1;
}
string h = hdrs +
SIP_HDR_COLSP(SIP_HDR_RACK) +
int2str(reply1xx.rseq) + " " +
int2str(reply1xx.cseq) + " " +
reply1xx.cseq_method + CRLF;
return sendRequest(SIP_METH_PRACK, body, h);
}
int AmSipDialog::cancel()
{
for(TransMap::reverse_iterator t = uac_trans.rbegin();
t != uac_trans.rend(); t++) {
if(t->second.method == SIP_METH_INVITE){
if(getStatus() != Cancelling){
setStatus(Cancelling);
return SipCtrlInterface::cancel(&t->second.tt, local_tag,
t->first, t->second.hdrs);
}
else {
ERROR("INVITE transaction has already been cancelled\n");
return -1;
}
}
}
ERROR("could not find INVITE transaction to cancel\n");
return -1;
}
int AmSipDialog::drop()
{
setStatus(Disconnected);
return 1;
}
int AmSipDialog::send_200_ack(unsigned int inv_cseq,
const AmMimeBody* body,
const string& hdrs,
int flags)
{
// TODO: implement missing pieces from RFC 3261:
// "The ACK MUST contain the same credentials as the INVITE. If
// the 2xx contains an offer (based on the rules above), the ACK MUST
// carry an answer in its body. If the offer in the 2xx response is not
// acceptable, the UAC core MUST generate a valid answer in the ACK and
// then send a BYE immediately."
TransMap::iterator inv_it = uac_trans.find(inv_cseq);
if (inv_it == uac_trans.end()) {
ERROR("trying to ACK a non-existing transaction (cseq=%i;local_tag=%s)\n",
inv_cseq,local_tag.c_str());
return -1;
}
AmSipRequest req;
req.method = SIP_METH_ACK;
req.r_uri = remote_uri;
req.from = SIP_HDR_COLSP(SIP_HDR_FROM) + local_party;
if(!ext_local_tag.empty())
req.from += ";tag=" + ext_local_tag;
else if(!local_tag.empty())
req.from += ";tag=" + local_tag;
req.to = SIP_HDR_COLSP(SIP_HDR_TO) + remote_party;
if(!remote_tag.empty())
req.to += ";tag=" + remote_tag;
req.cseq = inv_cseq;// should be the same as the INVITE
req.callid = callid;
req.contact = getContactHdr();
req.route = getRoute();
req.max_forwards = inv_it->second.max_forwards;
if(body != NULL)
req.body = *body;
if(onTxRequest(req,flags) < 0)
return -1;
if (!(flags&SIP_FLAGS_VERBATIM)) {
// add Signature
if (AmConfig::Signature.length())
req.hdrs += SIP_HDR_COLSP(SIP_HDR_USER_AGENT) + AmConfig::Signature + CRLF;
}
int res = SipCtrlInterface::send(req, local_tag,
remote_tag.empty() || !next_hop_1st_req ?
next_hop : "",
outbound_interface, 0, logger);
if (res)
return res;
onRequestTxed(req);
return 0;
}
/** EMACS **
* Local variables:
* mode: c++
* c-basic-offset: 2
* End:
*/