/*
* Copyright 2007 Sun Microsystems, Inc.
*
* This file is part of jVoiceBridge.
*
* jVoiceBridge is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation and distributed hereunder
* to you.
*
* jVoiceBridge 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, see <http://www.gnu.org/licenses/>.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied this
* code.
*/
package com.sun.voip.server;
import com.sun.voip.CallEvent;
import com.sun.voip.CallState;
import com.sun.voip.Logger;
import com.sun.voip.MediaInfo;
import com.sun.voip.SdpInfo;
import com.sun.voip.TreatmentManager;
import javax.sip.*;
import javax.sip.header.*;
import javax.sip.message.*;
import javax.sip.address.*;
import javax.sip.InvalidArgumentException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.io.IOException;
import java.text.ParseException;
import java.util.Vector;
/**
* Third Party Call Control SIP User Agent.
*
* SipTPCCallAgent VoIPGateway or SIP proxy
*
* |(1) INVITE with SDP |
* |---------------------->|
* |(2) 200 OK with SDP |
* |<----------------------|
* |(3) ACK with SDP |
* |---------------------->|
* |(4) treatment then RTP |
* |......................>|
*/
public class SipTPCCallAgent extends CallSetupAgent implements SipListener {
private SipServer.SipServerCallback sipServerCallback;
private SipUtil sipUtil;
private ClientTransaction clientTransaction; // most recent client trans
private String sdpBody;
private String sipCallId;
private boolean receivedBye;
private boolean callAnswered = false;
private static boolean forceGatewayError = false;
private boolean ackSent = false;
private boolean gotOk;
private TreatmentManager busyTreatment;
/**
* Constructor
* @param callHandler CallHandler this CallParticipant is associated with
*/
public SipTPCCallAgent(CallHandler callHandler) {
super(callHandler);
MediaInfo mixerMediaPreference = callHandler.getConferenceManager().getMediaInfo();
sipUtil = new SipUtil(mixerMediaPreference);
}
public static void forceGatewayError(boolean forceGatewayError) {
SipTPCCallAgent.forceGatewayError = forceGatewayError;
}
/*
* Begin Third-Party Call Control.
*/
public void initiateCall() throws IOException {
try {
try {
busyTreatment = new TreatmentManager("busy.au", 0);
} catch (IOException e) {
Logger.println("Invalid busy treatment: " + e.getMessage());
}
Logger.writeFile("Call " + cp
+ ": Begin SIP third party call");
setState(CallState.INVITED);
InetSocketAddress isa = callHandler.getReceiveAddress();
if (isa == null) {
throw new IOException("can't get receiver socket!");
}
// send INVITE to the CallParticipant
clientTransaction = sipUtil.sendInvite(cp, isa);
if (clientTransaction == null) {
Logger.error("Error placing call: " + cp);
setState(CallState.ENDED, "Reason='Error placing call'");
throw new IOException("Error placing call: " + cp);
}
CallIdHeader callIdHeader = (CallIdHeader)
clientTransaction.getRequest().getHeader(CallIdHeader.NAME);
sipCallId = callIdHeader.getCallId();
sipServerCallback = SipServer.getSipServerCallback();
sipServerCallback.addSipListener(sipCallId, this);
} catch (java.text.ParseException e) {
Logger.println("Call " + cp + " Error placing call " + cp +": "
+ e.getMessage());
setState(CallState.ENDED, "Reason='Error placing call " + cp + " "
+ e.getMessage() + "'");
throw new IOException("Error placing call " + cp + " "
+ e.getMessage());
} catch (InvalidArgumentException e) {
Logger.println("Call " + cp + " Error placing call " + cp +": "
+ e.getMessage());
setState(CallState.ENDED, "Reason='Error placing call " + cp + " "
+ e.getMessage() + "'");
throw new IOException("Error placing call " + cp + " "
+ e.getMessage());
} catch (SipException e) {
Logger.println("Call " + cp + " Error placing call " + cp +": "
+ e.getMessage());
setState(CallState.ENDED, "Reason='Error placing call " + cp + " "
+ e.getMessage() + "'");
throw new IOException("Error placing call " + cp + " "
+ e.getMessage());
}
}
/**
* Done with treatments for call end, now terminate the call
*/
public void terminateCall() {
if (receivedBye == false) {
if (gotOk == false ||
(getState() == CallState.INVITED && callAnswered == false)) {
try {
Logger.writeFile("Call " + cp + ": sendCancel");
sipUtil.sendCancel(clientTransaction);
} catch (Exception e) {
Logger.println("sendCancel " + e.getMessage());
}
} else {
/*
* Try sending a BYE as well.
* Seems that when we treat SESSION_PROGRESS
* as OK, sometimes we need to send a CANCEL
* and other times a BYE. We'll send both.
*/
try {
Logger.writeFile("Call " + cp + ": sendBye");
sipUtil.sendBye(clientTransaction);
} catch (Exception e) {
Logger.println("Call " + cp + ": sendBye"
+ e.getMessage());
}
}
}
}
/**
* Processes SIP requests. The only request being handled is BYE.
* @param requestReceivedEvent the event containing the SIP request
*/
public synchronized void processRequest(RequestEvent requestReceivedEvent) {
// obtain request and transaction id
Request request = requestReceivedEvent.getRequest();
ServerTransaction st = requestReceivedEvent.getServerTransaction();
if (request.getMethod().equals(Request.BYE)) {
handleBye(request, st);
} else if (request.getMethod().equals(Request.INVITE)) {
/*
* This is a re-Invite
*/
handleReInvite(request, st);
} else if (request.getMethod().equals(Request.ACK)) {
Logger.println("Call " + cp + " got ACK");
} else {
// no other requests should come in other than BYE, INVITE or ACK
Logger.writeFile("Call " + cp
+ " ignoring request " + request.getMethod());
}
}
/**
* handles a BYE request
* @param request the request
* @param transId the transaction Id
* @throws TransactionDoesNotExistException when the transaction
* record does not exist.
*/
private void handleBye(Request request, ServerTransaction st) {
try {
CallIdHeader callIdHeader = (CallIdHeader)
request.getHeader("Call-Id");
String sipCallId = callIdHeader.getCallId();
if (sipCallId.equals(this.sipCallId)) {
receivedBye = true;
try {
Logger.writeFile("Call " + cp + " has hung up.");
//sipUtil.sendOK(clientTransaction, st, cp);
sipUtil.sendOK(request, st);
} catch (Exception e) {
/*
* We sometimes get a null ServerTransaction
*/
}
cancelRequest("hung up");
sipServerCallback.removeSipListener(sipCallId);
} else {
/*
* this should not happen since the message has been
* delegated to this sip agent.
*/
throw new TransactionDoesNotExistException(
cp + "BYE request received did not "
+ "match either party: " + request);
}
} catch (TransactionDoesNotExistException e) {
Logger.error("Call " + cp
+ " Transaction not found " + e.getMessage());
} catch (SipException e) {
Logger.exception("Call " + cp + " SIP Stack error", e);
cancelRequest("handleBye: SIP Stack error " + e.getMessage());
} catch (Exception e) {
Logger.exception("Call " + cp + " Unknown error ", e);
cancelRequest("handleBye: SIP Stack error " + e.getMessage());
}
}
private void handleReInvite(Request request, ServerTransaction st) {
Logger.println("Call " + cp + " Re-INVITE\n" + request);
if (request.getRawContent() == null) {
Logger.error("Call " + cp + " no SDP in INVITE Request!");
return;
}
String sdpBody = new String(request.getRawContent());
SdpInfo sdpInfo;
try {
sdpInfo = sipUtil.getSdpInfo(sdpBody);
} catch (ParseException e) {
Logger.error("Call " + cp + " invalid SDP in re-INVITE Request! "
+ e.getMessage());
return;
}
MediaInfo mediaInfo = sdpInfo.getMediaInfo();
InetSocketAddress isa = new InetSocketAddress(
sdpInfo.getRemoteHost(), sdpInfo.getRemotePort());
InetSocketAddress rtcpAddress = sdpInfo.getRtcpAddress();
setEndpointAddress(isa, mediaInfo.getPayload(),
sdpInfo.getTransmitMediaInfo().getPayload(),
sdpInfo.getTelephoneEventPayload(), rtcpAddress);
isa = callHandler.getReceiveAddress();
try {
sipUtil.sendOkWithSdp(request, st, isa, sdpInfo);
} catch (Exception e) {
Logger.println("Call " + cp +
" Failed to send ok with sdp for re-invite " + e.getMessage());
return;
}
}
/**
* Processes SIP responses.
* @param responseReceivedEvent the event containing the SIP response
*
* Note: This method is called by a thread from the SIP stack.
* If it blocks, no other events will be delivered!
*/
public static boolean rejectCall = false; // for debugging!
public synchronized void processResponse(
ResponseEvent responseReceivedEvent) {
try {
Response response = (Response)responseReceivedEvent.getResponse();
ClientTransaction clientTransaction =
responseReceivedEvent.getClientTransaction();
int statusCode = response.getStatusCode();
FromHeader fromHeader = (FromHeader)
response.getHeader(FromHeader.NAME);
String displayName = fromHeader.getAddress().getDisplayName();
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("Response: statusCode "
+ statusCode + " state " + getCallState()
+ " fromHeader " + displayName + " call participant "
+ cp.getName());
}
if (reasonCallTerminated != null) {
/*
* Ignore OK and Request Terminated.
* XXX what's the symbol for 487?
*/
if (statusCode != Response.OK && statusCode != 487) {
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("Call " + cp
+ ": request cancelled, ignoring response");
}
}
CallIdHeader callIdHeader = (CallIdHeader)
response.getHeader("Call-Id");
String sipCallId = callIdHeader.getCallId();
sipServerCallback.removeSipListener(sipCallId);
return;
}
/*
* Some type of global failure that prevents the
* CallParticipant from being contacted, report failure.
*/
if (forceGatewayError) {
statusCode = 500;
forceGatewayError = false;
}
if (statusCode >= 500 && getState() == CallState.INVITED) {
Logger.error("Call " + cp + " gateway error: " + statusCode
+ " " + response.getReasonPhrase());
cancelRequest("gateway error: " + statusCode + " " + response.getReasonPhrase());
return;
} else if (statusCode == Response.PROXY_AUTHENTICATION_REQUIRED || statusCode == Response.UNAUTHORIZED) {
if (cp.getProxyCredentials() != null)
{
try {
SipServer.handleChallenge(response, clientTransaction, cp.getProxyCredentials()).sendRequest();
} catch (Exception e) {
Logger.println("Proxy authentification failed " + e);
}
}
return;
} else if (statusCode >= 400) {
// if we get a busy or an unknown error, play busy.
Logger.println("Call " + cp + " got status code :" + statusCode);
cp.setCallEndTreatment(null);
cp.setConferenceJoinTreatment(null);
cp.setConferenceLeaveTreatment(null);
/*
* play busy treatment, but deallocate any resources
* held up by ringBack first, if any.
*/
//stopCallAnsweredTreatment();
if (statusCode == Response.BUSY_HERE) {
try {
if (busyTreatment != null) {
addTreatment(busyTreatment);
//busyTreatment.waitForTreatment();
} else {
Logger.println("Unable to play busy treatment!!!");
}
} catch (Exception e) {
Logger.error("can't start busy treatment!" + sdpBody);
}
CallEvent callEvent = new CallEvent(CallEvent.BUSY_HERE);
callEvent.setInfo(response.getReasonPhrase());
sendCallEventNotification(callEvent);
}
//sipUtil.sendBye(clientTransaction);
cancelRequest(response.getReasonPhrase());
return;
}
/* state machine */
switch (getState()) {
/*
* CallParticipant picked up, send treatment if any,
* and wait for it to finish.
*/
case CallState.INVITED:
if (rejectCall) {
Logger.error("Call " + cp + " gateway error: "
+ statusCode + " " + response.getReasonPhrase());
cancelRequest("gateway error: " + statusCode + " "
+ response.getReasonPhrase());
return;
}
handleCallParticipantInvited(response, clientTransaction);
break;
/*
* Call established, the ACK needs to be resent.
* According to Ranga, this is done by the NIST SIP Stack.
*/
case CallState.ESTABLISHED:
if (statusCode == Response.OK) {
gotOk = true;
Logger.writeFile("Call " + cp + " Got OK, ESTABLISHED");
if (ackSent == false) {
sipUtil.sendAck(clientTransaction);
ackSent = true;
}
}
break;
case CallState.ENDED:
break; // ignore the response
default:
Logger.error("Process Response bad state " + getState()
+ "\n" + response);
}
} catch (SipException e) {
Logger.exception("Call " + cp + " SIP Stack error ", e);
cancelRequest(
"processResponse: SIP Stack error " + e.getMessage());
} catch (Exception e) {
Logger.exception("processResponse: " + cp, e);
cancelRequest(
"processResponse: SIP Stack error " + e.getMessage());
}
}
/**
* handles the INVITED state.
* @param response the response
* @param clientTransaction the client transaction
* @throws SipException SIP stack related error
*/
private void handleCallParticipantInvited(Response response, ClientTransaction clientTransaction) throws ParseException, SipException, InvalidArgumentException
{
FromHeader fromHeader = (FromHeader)
response.getHeader(FromHeader.NAME);
String displayName = fromHeader.getAddress().getDisplayName();
int statusCode = response.getStatusCode();
Logger.println("handleCallParticipantInvited " + cp + " status " + statusCode + " " + response.getReasonPhrase());
Logger.println("handleCallParticipantInvited , displayname " + displayName);
CallIdHeader callIdHeader = (CallIdHeader) response.getHeader(CallIdHeader.NAME);
if (sipCallId.equals(callIdHeader.getCallId()) &&
displayName.equals(cp.getDisplayName()) &&
(statusCode == Response.OK || statusCode == Response.SESSION_PROGRESS) &&
((CSeqHeader)response.getHeader(CSeqHeader.NAME)).getMethod().equals(Request.INVITE))
{
if (statusCode == Response.SESSION_PROGRESS) {
/*
* For some calls, we never get an OK. Instead we just get
* SESSION_PROGRESS. In order to handle these calls, we treat
* SESSION_PROGRESS as OK. If an OK arrives later, we'll
* send an ACK. This flag allows us to enable or
* disable this workaround for each call.
*
* The problem with always treating SESSION_PROGRESS as OK
* is that in a conference everybody will hear the ringing sound
* which the remote call sends until the call is actually answered.
* This can be avoided if joinConfirmation is specified.
* The other problem is that if we treat SESSION_PROGRESS
* as though the call has been answered, then we'll start
* playing the treatment before a person really answers to
* hear the treatment.
*/
if (cp.getHandleSessionProgress() == false) {
Logger.writeFile("Call " + cp + " Ignoring SESSION_PROGRESS");
return;
}
Logger.writeFile("Call " + cp + " Treating SESSION_PROGRESS as OK");
}
if (response.getRawContent() == null)
{
Logger.error("Call " + cp + " no SDP in OK Response!");
cancelRequest("SIP error! no SDP in OK Response!");
return;
}
this.clientTransaction = clientTransaction;
if (statusCode == Response.OK)
{
gotOk = true;
Logger.writeFile("Call " + cp + " Got OK, call answered\n" + response);
}
ToHeader toHeader = (ToHeader)response.getHeader(ToHeader.NAME);
/*
* We got an OK response.
*
* send an ACK back to the CallParticipant
*/
if (statusCode == Response.OK) {
sipUtil.sendAck(clientTransaction);
ackSent = true;
}
if (callAnswered) {
Logger.writeFile("Call " + cp + " done processing OK");
return;
}
/*
* Remember the IP and port of where to send data to
* the CallParticipant.
*/
sdpBody = new String(response.getRawContent());
SdpInfo sdpInfo;
try {
sdpInfo = sipUtil.getSdpInfo(sdpBody, false);
} catch (ParseException e) {
Logger.error("Call " + cp + " Invalid SDP in OK Response! " + e.getMessage());
cancelRequest("SIP error! Invalid SDP in OK Response!");
return;
}
MediaInfo mediaInfo = sdpInfo.getMediaInfo();
InetSocketAddress isa = new InetSocketAddress(sdpInfo.getRemoteHost(), sdpInfo.getRemotePort());
InetSocketAddress rtcpAddress = sdpInfo.getRtcpAddress();
setEndpointAddress(isa, mediaInfo.getPayload(),sdpInfo.getTransmitMediaInfo().getPayload(),sdpInfo.getTelephoneEventPayload(), rtcpAddress);
/*
* The CallParticipant has answered.
* If join confirmation is required, we remain in the
* INVITED state. We set the callAnswered flag so that
* if the join confirmation times out we know to
* send a BYE rather than a CANCEL.
*/
callAnswered = true;
if (cp.getJoinConfirmationTimeout() == 0) {
setState(CallState.ANSWERED);
}
/*
* Start treatment if any and wait for it to finish.
* When the treatment finishes, notification will
* be delivered to our parent which will indicate
* we're ready for the conference.
*
* If there's no treatment to be played, we're ready now
* unless we're waiting for join confirmation..
*/
initializeCallAnsweredTreatment();
if (callAnsweredTreatment != null) {
startCallAnsweredTreatment();
} else {
if (cp.getJoinConfirmationTimeout() == 0) {
setState(CallState.ESTABLISHED);
}
}
} else {
Logger.writeFile("Call " + cp + " Ignoring response: " + response.getReasonPhrase());
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("Call " + cp + " Response: " + response);
}
}
}
/**
* Processes a retransmit or expiration Timeout of an underlying
* {@link Transaction} handled by this SipListener. This Event notifies the
* application that a retransmission or transaction Timer expired in the
* SipProvider's transaction state machine. The TimeoutEvent encapsulates
* the specific timeout type and the transaction identifier either client
* or server upon which the timeout occured. The type of Timeout can by
* determined by:
* <code>timeoutType = timeoutEvent.getTimeout().getValue();</code>
*
* @param timeoutEvent - the timeoutEvent received indicating either the
* message retransmit or transaction timed out.
*/
public void processTimeout(TimeoutEvent timeoutEvent) {
}
public void processDialogTerminated(DialogTerminatedEvent dte) {
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("processDialogTerminated called");
}
}
public void processTransactionTerminated(TransactionTerminatedEvent tte) {
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("processTransactionTerminated called");
}
}
public void processIOException(IOExceptionEvent ioee) {
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("processTransactionTerminated called");
}
}
}