/* * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.mobicents.servlet.sip.proxy; import gov.nist.javax.sip.message.SIPMessage; import gov.nist.javax.sip.stack.SIPClientTransaction; import gov.nist.javax.sip.stack.SIPTransaction; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.Timer; import javax.servlet.ServletException; import javax.servlet.sip.Proxy; import javax.servlet.sip.ProxyBranch; import javax.servlet.sip.ServletParseException; import javax.servlet.sip.SipServletRequest; import javax.servlet.sip.SipServletResponse; import javax.servlet.sip.SipURI; import javax.servlet.sip.URI; import javax.servlet.sip.ar.SipApplicationRoutingDirective; import javax.sip.ClientTransaction; import javax.sip.SipException; import javax.sip.SipProvider; import javax.sip.header.RouteHeader; import javax.sip.header.ViaHeader; import javax.sip.message.Request; import org.apache.log4j.Logger; import org.mobicents.servlet.sip.JainSipUtils; import org.mobicents.servlet.sip.address.SipURIImpl; import org.mobicents.servlet.sip.core.RoutingState; import org.mobicents.servlet.sip.core.SipApplicationDispatcherImpl; import org.mobicents.servlet.sip.core.dispatchers.MessageDispatcher; import org.mobicents.servlet.sip.core.session.MobicentsSipSession; import org.mobicents.servlet.sip.message.SipServletRequestImpl; import org.mobicents.servlet.sip.message.SipServletResponseImpl; import org.mobicents.servlet.sip.message.TransactionApplicationData; /** * @author root * */ public class ProxyBranchImpl implements ProxyBranch, Serializable { private static final long serialVersionUID = 1L; private static transient Logger logger = Logger.getLogger(ProxyBranchImpl.class); private ProxyImpl proxy; private transient SipServletRequestImpl originalRequest; private transient SipServletRequestImpl prackOriginalRequest; private transient SipServletRequestImpl outgoingRequest; private transient SipServletResponseImpl lastResponse; private transient URI targetURI; private transient SipURI outboundInterface; private transient SipURI recordRouteURI; private boolean recordRoutingEnabled; private boolean recurse; private transient SipURI pathURI; private boolean started; private boolean timedOut; private int proxyBranchTimeout; private transient ProxyBranchTimerTask proxyTimeoutTask; private transient boolean proxyBranchTimerStarted; private transient Object cTimerLock; private boolean canceled; private boolean isAddToPath; private transient List<ProxyBranch> recursedBranches; private boolean waitingForPrack; public transient ViaHeader viaHeader; private static transient Timer timer = new Timer(); public ProxyBranchImpl(URI uri, ProxyImpl proxy) { this.targetURI = uri; this.proxy = proxy; isAddToPath = proxy.getAddToPath(); this.originalRequest = (SipServletRequestImpl) proxy.getOriginalRequest(); this.recordRouteURI = proxy.recordRouteURI; this.pathURI = proxy.pathURI; this.outboundInterface = proxy.getOutboundInterface(); if(recordRouteURI != null) { this.recordRouteURI = (SipURI)((SipURIImpl)recordRouteURI).clone(); } this.proxyBranchTimeout = proxy.getProxyTimeout(); this.canceled = false; this.recursedBranches = new ArrayList<ProxyBranch>(); proxyBranchTimerStarted = false; cTimerLock = new Object(); // Here we create a clone which is available through getRequest(), the user can add // custom headers and push routes here. Later when we actually proxy the request we // will clone this request (with it's custome headers and routes), but we will override // the modified RR and Path parameters (as defined in the spec). Request cloned = (Request)originalRequest.getMessage().clone(); ((SIPMessage)cloned).setApplicationData(null); this.outgoingRequest = new SipServletRequestImpl( cloned, proxy.getSipFactoryImpl(), this.originalRequest.getSipSession(), null, null, false); } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#cancel() */ public void cancel() { cancel(null, null, null); } /* * (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#cancel(java.lang.String[], int[], java.lang.String[]) */ public void cancel(String[] protocol, int[] reasonCode, String[] reasonText) { if(proxy.getAckReceived()) throw new IllegalStateException("There has been an ACK received on this branch. Can not cancel."); try { cancelTimer(); if(this.isStarted() && !canceled && !timedOut && outgoingRequest.getMethod().equalsIgnoreCase(Request.INVITE)) { if(lastResponse != null) { /* According to SIP RFC we should send cancel only if we receive any response first*/ SipServletRequest cancelRequest = outgoingRequest.createCancel(); //Adding reason headers if needed if(protocol != null && reasonCode != null && reasonText != null && protocol.length == reasonCode.length && reasonCode.length == reasonText.length) { for (int i = 0; i < protocol.length; i++) { ((SipServletRequestImpl)cancelRequest). addHeaderInternal("Reason", protocol[i] + ";cause=" + reasonCode[i] + ";text=\"" + reasonText[i] + "\"", false); } } cancelRequest.send(); } else { // We dont send cancel, but we must stop the invite retrans SIPClientTransaction tx = (SIPClientTransaction) outgoingRequest.getTransaction(); Method disableRetransmissionTimer = SIPTransaction.class.getDeclaredMethod("disableRetransmissionTimer"); Method disableTimeoutTimer = SIPTransaction.class.getDeclaredMethod("disableTimeoutTimer"); disableRetransmissionTimer.setAccessible(true); disableTimeoutTimer.setAccessible(true); disableRetransmissionTimer.invoke(tx); disableTimeoutTimer.invoke(tx); /* try { //tx.terminate(); // Do not terminate the tx here, because ProxyCancel test is failing. If the tx // is terminated 100 Trying is dropped at JSIP. } catch(Exception e2) { logger.error("Can not terminate transaction", e2); }*/ } canceled = true; } if(!this.isStarted() && outgoingRequest.getMethod().equalsIgnoreCase(Request.INVITE)) { canceled = true; } } catch(Exception e) { throw new IllegalStateException("Failed canceling proxy branch", e); } } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#getProxy() */ public Proxy getProxy() { return proxy; } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#getProxyBranchTimeout() */ public int getProxyBranchTimeout() { return proxyBranchTimeout; } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#getRecordRouteURI() */ public SipURI getRecordRouteURI() { if(this.getRecordRoute()) { if(this.recordRouteURI == null) this.recordRouteURI = proxy.getSipFactoryImpl().createSipURI("proxy", "localhost"); return this.recordRouteURI; } else throw new IllegalStateException("Record Route not enabled for this ProxyBranch. You must call proxyBranch.setRecordRoute(true) before getting an URI."); } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#getRecursedProxyBranches() */ public List<ProxyBranch> getRecursedProxyBranches() { return recursedBranches; } public void addRecursedBranch(ProxyBranchImpl branch) { recursedBranches.add(branch); } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#getRequest() */ public SipServletRequest getRequest() { return outgoingRequest; } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#getResponse() */ public SipServletResponse getResponse() { return lastResponse; } public void setResponse(SipServletResponse response) { lastResponse = (SipServletResponseImpl) response; } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#isStarted() */ public boolean isStarted() { return started; } /* (non-Javadoc) * @see javax.servlet.sip.ProxyBranch#setProxyBranchTimeout(int) */ public void setProxyBranchTimeout(int seconds) { if(seconds<=0) throw new IllegalArgumentException("Negative or zero timeout not allowed"); if(isCanceled() || isTimedOut()) { logger.error("Cancelled or timed out proxy branch should not be updated with new timeout values"); return; } this.proxyBranchTimeout = seconds; if(this.started) updateTimer(); } /** * After the branch is initialized, this method proxies the initial request to the * specified destination. Subsequent requests are proxied through proxySubsequentRequest */ public void start() { if(started) { throw new IllegalStateException("Proxy branch alredy started!"); } if(canceled) { throw new IllegalStateException("Proxy branch was cancelled, you must create a new branch!"); } if(timedOut) { throw new IllegalStateException("Proxy brnach has timed out!"); } if(proxy.getAckReceived()) { throw new IllegalStateException("An ACK request has been received on this proxy. Can not start new branches."); } // Initialize these here for efficiency. updateTimer(); SipURI recordRoute = null; // If the proxy is not adding record-route header, set it to null and it // will be ignored in the Proxying if(proxy.getRecordRoute() || this.getRecordRoute()) { if(recordRouteURI == null) { recordRouteURI = proxy.getSipFactoryImpl().createSipURI("proxy", "localhost"); } recordRoute = recordRouteURI; } Request cloned = proxy.getProxyUtils().createProxiedRequest( outgoingRequest, this, new ProxyParams(this.targetURI, this.outboundInterface, recordRoute, this.pathURI)); //tells the application dispatcher to stop routing the original request //since it has been proxied originalRequest.setRoutingState(RoutingState.PROXIED); forwardRequest(cloned, false); started = true; } /** * Forward the request to the specified destination. The method is used internally. * @param request * @param subsequent Set to false if the the method is initial */ private void forwardRequest(Request request, boolean subsequent) { if(logger.isDebugEnabled()) { logger.debug("creating cloned Request for proxybranch " + request); } SipServletRequestImpl clonedRequest = new SipServletRequestImpl( request, proxy.getSipFactoryImpl(), null, null, null, false); if(subsequent) { clonedRequest.setRoutingState(RoutingState.SUBSEQUENT); } this.outgoingRequest = clonedRequest; // Initialize the sip session for the new request if initial clonedRequest.setCurrentApplicationName(originalRequest.getCurrentApplicationName()); if(clonedRequest.getCurrentApplicationName() == null && subsequent) { clonedRequest.setCurrentApplicationName(originalRequest.getSipSession().getSipApplicationSession().getApplicationName()); } clonedRequest.setSipSessionKey(originalRequest.getSipSession().getKey()); MobicentsSipSession newSession = (MobicentsSipSession) clonedRequest.getSession(true); try { newSession.setHandler(((MobicentsSipSession)this.originalRequest.getSession()).getHandler()); } catch (ServletException e) { logger.error("could not set the session handler while forwarding the request", e); throw new RuntimeException(e); } // Use the original dialog in the new session newSession.setSessionCreatingDialog(originalRequest.getSipSession().getSessionCreatingDialog()); // And set a reference to the proxy newSession.setProxy(proxy); //JSR 289 Section 15.1.6 if(!subsequent) { // Subsequent requests can't have a routing directive? clonedRequest.setRoutingDirective(SipApplicationRoutingDirective.CONTINUE, originalRequest); } clonedRequest.getTransactionApplicationData().setProxyBranch(this); clonedRequest.send(); } /** * A callback. Here we receive all responses from the proxied requests we have sent. * * @param response */ public void onResponse(SipServletResponseImpl response) { // If we are canceled but still receiving provisional responses try to cancel them if(canceled && response.getStatus() < 200) { try { SipServletRequest cancelRequest = outgoingRequest.createCancel(); cancelRequest.send(); } catch (Exception e) { if(logger.isDebugEnabled()) { logger.debug("Failed to cancel again a provisional response " + response.toString() , e); } } } // We have already sent TRYING, don't send another one if(response.getStatus() == 100) return; // Send informational responses back immediately if((response.getStatus() > 100 && response.getStatus() < 200) || (response.getStatus() == 200 && Request.PRACK.equals(response.getMethod()))) { // Deterimine if the response is reliable. We just look at RSeq, because // every such response is required to have it. if(response.getHeader("RSeq") != null) { this.setWaitingForPrack(true); // this branch is expecting a PRACK now } SipServletResponse proxiedResponse = proxy.getProxyUtils().createProxiedResponse(response, this); if(proxiedResponse == null) return; // this response was addressed to this proxy try { proxiedResponse.send(); } catch (IOException e) { logger.error("A problem occured while proxying a response", e); } return; } // Non-provisional responses must also cancel the timer, otherwise it will timeout // and return multiple responses for a single transaction. cancelTimer(); if(response.getStatus() >= 600) // Cancel all 10.2.4 this.proxy.cancelAllExcept(this, null, null, null, false); // FYI: ACK is sent automatically by jsip when needed boolean recursed = false; if(response.getStatus() >= 300 && response.getStatus()<400 && recurse) { String contact = response.getHeader("Contact"); if(contact != null) { //javax.sip.address.SipURI uri = SipFactories.addressFactory.createAddress(contact); try { int start = contact.indexOf('<'); int end = contact.indexOf('>'); contact = contact.substring(start + 1, end); URI uri = proxy.getSipFactoryImpl().createURI(contact); ArrayList<SipURI> list = new ArrayList<SipURI>(); list.add((SipURI)uri); List<ProxyBranch> pblist = proxy.createProxyBranches(list); ProxyBranchImpl pbi = (ProxyBranchImpl)pblist.get(0); this.addRecursedBranch(pbi); pbi.start(); recursed = true; } catch (ServletParseException e) { throw new RuntimeException("Can not parse contact header", e); } } } if(response.getStatus() >= 200 && !recursed) { if(outgoingRequest != null && outgoingRequest.isInitial()) { this.proxy.onFinalResponse(this); } else { this.proxy.sendFinalResponse(response, this); } } } /** * Has the branch timed out? * * @return */ public boolean isTimedOut() { return timedOut; } /** * Call this method when a subsequent request must be proxied through the branch. * * @param request */ public void proxySubsequentRequest(SipServletRequestImpl request) { // A re-INVITE needs special handling without goind through the dialog-stateful methods if(request.getMethod().equalsIgnoreCase("INVITE")) { if(logger.isDebugEnabled()) { logger.debug("Proxying reinvite request " + request); } // reset the ack received flag for reINVITE request.getSipSession().setAckReceived(false); proxyDialogStateless(request); return; } if(logger.isDebugEnabled()) { logger.debug("Proxying subsequent request " + request); } // Update the last proxied request request.setRoutingState(RoutingState.PROXIED); proxy.setOriginalRequest(request); this.originalRequest = request; // No proxy params, sine the target is already in the Route headers ProxyParams params = new ProxyParams(null, null, null, null); Request clonedRequest = proxy.getProxyUtils().createProxiedRequest(request, this, params); // There is no need for that, it makes application composition fail (The subsequent request is not dispatched to the next application since the route header is removed) // RouteHeader routeHeader = (RouteHeader) clonedRequest.getHeader(RouteHeader.NAME); // if(routeHeader != null) { // if(!((SipApplicationDispatcherImpl)proxy.getSipFactoryImpl().getSipApplicationDispatcher()).isRouteExternal(routeHeader)) { // clonedRequest.removeFirst(RouteHeader.NAME); // } // } try { // Reset the proxy supervised state to default Chapter 6.2.1 - page down list bullet number 6 proxy.setSupervised(true); if(clonedRequest.getMethod().equalsIgnoreCase(Request.ACK) ) { //|| clonedRequest.getMethod().equalsIgnoreCase(Request.PRACK)) { String transport = JainSipUtils.findTransport(clonedRequest); SipProvider sipProvider = proxy.getSipFactoryImpl().getSipNetworkInterfaceManager().findMatchingListeningPoint( transport, false).getSipProvider(); sipProvider.sendRequest(clonedRequest); } else { forwardRequest(clonedRequest, true); } } catch (SipException e) { logger.error("A problem occured while proxying a subsequent request", e); } } /** * This method proxies requests without updating JSIP dialog state. PRACK and re-INVITE * requests require this kind of handling because: * 1. PRACK occurs before a dialog has been established (and also produces OKs before * the final response) * 2. re-INVITE when sent with the dialog method resets the internal JSIP CSeq counter * to 1 every time you need it, which causes issues like * http://groups.google.com/group/mobicents-public/browse_thread/thread/1a22ccdc4c481f47 * * @param request */ public void proxyDialogStateless(SipServletRequestImpl request) { if(logger.isDebugEnabled()) { logger.debug("Proxying request dialog-statelessly" + request); } // Update the last proxied request request.setRoutingState(RoutingState.PROXIED); this.prackOriginalRequest = request; URI targetURI = this.targetURI; // Determine the direction of the request. Either it's from the dialog initiator (the caller) // or from the callee if(!request.getFrom().toString().equals(proxy.getCallerFromHeader())) { // If it's from the callee we should send it in the other direction targetURI = proxy.getPreviousNode(); } ProxyParams params = new ProxyParams(targetURI, null, null, null); Request clonedRequest = proxy.getProxyUtils().createProxiedRequest(request, this, params); ViaHeader viaHeader = (ViaHeader) clonedRequest.getHeader(ViaHeader.NAME); try { viaHeader.setParameter(MessageDispatcher.RR_PARAM_APPLICATION_NAME, proxy.getSipFactoryImpl().getSipApplicationDispatcher().getHashFromApplicationName(request.getSipSession().getKey().getApplicationName())); viaHeader.setParameter(MessageDispatcher.APP_ID, request.getSipSession().getSipApplicationSession().getKey().getId()); } catch (ParseException pe) { logger.error("A problem occured while proxying a request in a dialog-stateless transaction", pe); } RouteHeader routeHeader = (RouteHeader) clonedRequest.getHeader(RouteHeader.NAME); if(routeHeader != null) { if(!((SipApplicationDispatcherImpl)proxy.getSipFactoryImpl().getSipApplicationDispatcher()).isRouteExternal(routeHeader)) { clonedRequest.removeFirst(RouteHeader.NAME); } } String transport = JainSipUtils.findTransport(clonedRequest); SipProvider sipProvider = proxy.getSipFactoryImpl().getSipNetworkInterfaceManager().findMatchingListeningPoint( transport, false).getSipProvider(); if(logger.isDebugEnabled()) { logger.debug("Getting new Client Tx for request " + clonedRequest); } try { ClientTransaction ctx = sipProvider .getNewClientTransaction(clonedRequest); TransactionApplicationData appData = (TransactionApplicationData) request.getTransactionApplicationData(); appData.setProxyBranch(this); ctx.setApplicationData(appData); ctx.sendRequest(); } catch (SipException e) { logger.error("A problem occured while proxying a request in a dialog-stateless transaction", e); } } /** * This callback is called when the remote side has been idle too long while * establishing the dialog. * */ public void onTimeout() { this.cancel(); this.timedOut = true; // Just do a timeout response proxy.onBranchTimeOut(this); } /** * Restart the timer. Call this method when some activity shows the remote * party is still online. * */ void updateTimer() { cancelTimer(); if(proxyBranchTimeout > 0) { synchronized (cTimerLock) { if(!proxyBranchTimerStarted) { try { ProxyBranchTimerTask timerCTask = new ProxyBranchTimerTask(this); if(logger.isDebugEnabled()) { logger.debug("Proxy Branch Timeout set to " + proxyBranchTimeout); } timer.schedule(timerCTask, proxyBranchTimeout * 1000L); proxyTimeoutTask = timerCTask; proxyBranchTimerStarted = true; } catch (IllegalStateException e) { logger.error("Unexpected exception while scheduling Timer C" ,e); } } } } } /** * Stop the C Timer. */ public void cancelTimer() { synchronized (cTimerLock) { if(proxyTimeoutTask != null && proxyBranchTimerStarted) { proxyTimeoutTask.cancel(); proxyTimeoutTask = null; proxyBranchTimerStarted = false; } } } public boolean isCanceled() { return canceled; } /** * {@inheritDoc} */ public boolean getAddToPath() { return isAddToPath; } /** * {@inheritDoc} */ public SipURI getPathURI() { if(!isAddToPath) { throw new IllegalStateException("addToPath is not enabled!"); } return this.pathURI; } /** * {@inheritDoc} */ public boolean getRecordRoute() { return recordRoutingEnabled; } /** * {@inheritDoc} */ public boolean getRecurse() { return recurse; } /** * {@inheritDoc} */ public void setAddToPath(boolean isAddToPath) { if(started) { throw new IllegalStateException("Cannot set a record route on an already started proxy"); } if(this.pathURI == null) { this.pathURI = new SipURIImpl ( JainSipUtils.createRecordRouteURI( proxy.getSipFactoryImpl().getSipNetworkInterfaceManager(), null)); } this.isAddToPath = isAddToPath; } /** * {@inheritDoc} */ public void setOutboundInterface(InetAddress inetAddress) { //TODO check against our defined outbound interfaces checkSessionValidity(); String address = inetAddress.getHostAddress(); outboundInterface = proxy.getSipFactoryImpl().createSipURI(null, address); } /** * {@inheritDoc} */ public void setOutboundInterface(InetSocketAddress inetSocketAddress) { //TODO check against our defined outbound interfaces checkSessionValidity(); String address = inetSocketAddress.getAddress().getHostAddress() + ":" + inetSocketAddress.getPort(); outboundInterface = proxy.getSipFactoryImpl().createSipURI(null, address); } /** * {@inheritDoc} */ public void setRecordRoute(boolean isRecordRoute) { recordRoutingEnabled = isRecordRoute; } /** * {@inheritDoc} */ public void setRecurse(boolean isRecurse) { recurse = isRecurse; } private void checkSessionValidity() { if(this.originalRequest.getApplicationSession().isValid() && this.originalRequest.getSession().isValid()) return; throw new IllegalStateException("Invalid session."); } /** * @param prackOriginalRequest the prackOriginalRequest to set */ public void setPrackOriginalRequest(SipServletRequestImpl prackOriginalRequest) { this.prackOriginalRequest = prackOriginalRequest; } /** * @return the prackOriginalRequest */ public SipServletRequestImpl getPrackOriginalRequest() { return prackOriginalRequest; } public boolean isWaitingForPrack() { return waitingForPrack; } public void setWaitingForPrack(boolean waitingForPrack) { this.waitingForPrack = waitingForPrack; } }