/* * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.openfire.sip.tester.security; import org.jivesoftware.openfire.sip.tester.Log; import org.jivesoftware.openfire.sip.tester.stack.SIPConfig; import org.jivesoftware.openfire.sip.tester.stack.SipManager; import javax.sip.ClientTransaction; import javax.sip.InvalidArgumentException; import javax.sip.SipException; import javax.sip.SipProvider; import javax.sip.address.Address; import javax.sip.address.SipURI; import javax.sip.address.URI; import javax.sip.header.*; import javax.sip.message.Request; import javax.sip.message.Response; import java.text.ParseException; import java.util.ListIterator; /** * <p/> * Title: Netsite TudoMais * </p> * <p/> * Description:JAIN-SIP Audio/Video phone application * </p> * <p/> * Copyright: Copyright (c) 2006 * </p> * <p/> * Organisation: CTBC Telecom / Netsite * </p> * * @author Thiago Rocha Camargo (thiago@jivesoftware.com) */ public class SipSecurityManager { /** */ private HeaderFactory headerFactory = null; /** */ private SipProvider transactionCreator = null; private SipManager sipManCallback = null; /** * Credentials cached so far. */ CredentialsCache cachedCredentials = new CredentialsCache(); public UserCredentials defaultCredentials = null; public SipSecurityManager() { } public UserCredentials getDefaultCredentials() { return defaultCredentials; } public void setDefaultCredentials(UserCredentials defaultCredentials) { this.defaultCredentials = defaultCredentials; } /** * set the header factory to be used when creating authorization headers * * @uml.property name="headerFactory" */ public void setHeaderFactory(HeaderFactory headerFactory) { this.headerFactory = headerFactory; } /** * Verifies whether there are any user credentials registered for the call * that "request" belongs to and appends corresponding authorization headers * if that is the case. * * @param request the request that needs to be attached credentials. */ public void appendCredentialsIfNecessary(Request request) { // TODO IMPLEMENT } /** * Uses securityAuthority to determinie a set of valid user credentials for * the specified Response (Challenge) and appends it to the challenged * request so that it could be retransmitted. * <p/> * Fredrik Wickstrom reported that dialog cseq counters are not incremented * when resending requests. He later uncovered additional problems and * proposed a way to fix them (his proposition was taken into account). * * @param challenge the 401/407 challenge response * @param challengedTransaction the transaction established by the challenged request * @return a transaction containing a reoriginated request with the * necessary authorization header. * @throws SipSecurityException */ public ClientTransaction handleChallenge(Response challenge, ClientTransaction challengedTransaction) throws SipSecurityException, SipException, InvalidArgumentException, ParseException { try { String branchID = challengedTransaction.getBranchId(); Request challengedRequest = challengedTransaction.getRequest(); Request reoriginatedRequest = (Request) challengedRequest.clone(); ListIterator authHeaders = null; if (challenge == null || reoriginatedRequest == null) throw new NullPointerException( "A null argument was passed to handle challenge."); // CallIdHeader callId = // (CallIdHeader)challenge.getHeader(CallIdHeader.NAME); if (challenge.getStatusCode() == Response.UNAUTHORIZED) authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME); else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED) authHeaders = challenge .getHeaders(ProxyAuthenticateHeader.NAME); if (authHeaders == null) throw new SecurityException( "Could not find WWWAuthenticate or ProxyAuthenticate headers"); // Remove all authorization headers from the request (we'll re-add // them // from cache) reoriginatedRequest.removeHeader(AuthorizationHeader.NAME); reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME); // rfc 3261 says that the cseq header should be augmented for the // new // request. do it here so that the new dialog (created together with // the new client transaction) takes it into account. // Bug report - Fredrik Wickstrom CSeqHeader cSeq = (CSeqHeader) reoriginatedRequest .getHeader((CSeqHeader.NAME)); cSeq.setSequenceNumber(cSeq.getSequenceNumber() + 1); ClientTransaction retryTran = transactionCreator .getNewClientTransaction(reoriginatedRequest); WWWAuthenticateHeader authHeader = null; CredentialsCacheEntry ccEntry = null; while (authHeaders.hasNext()) { authHeader = (WWWAuthenticateHeader) authHeaders.next(); String realm = authHeader.getRealm(); // Check whether we have cached credentials for authHeader's // realm // make sure that if such credentials exist they get removed. // The // challenge means that there's something wrong with them. ccEntry = cachedCredentials.remove(realm); // Try to guess user name and facilitate user UserCredentials defaultCredentials = new UserCredentials(); FromHeader from = (FromHeader) reoriginatedRequest .getHeader(FromHeader.NAME); URI uri = from.getAddress().getURI(); if (uri.isSipURI()) { Log.debug("handleChallenge", SIPConfig.getAuthUserName()); String user = SIPConfig.getAuthUserName() != null ? SIPConfig .getAuthUserName() : ((SipURI) uri).getUser(); defaultCredentials.setAuthUserName(user == null ? SIPConfig .getUserName() : user); } boolean ccEntryHasSeenTran = false; if (ccEntry != null) ccEntryHasSeenTran = ccEntry.processResponse(branchID); // get a new pass if (ccEntry == null // we don't have credentials for the // specified realm || ((!authHeader.isStale() && ccEntryHasSeenTran))) { if (ccEntry == null) { ccEntry = new CredentialsCacheEntry(); ccEntry.userCredentials = defaultCredentials; } // put the returned user name in the properties file // so that it appears as a default one next time user is // prompted for pass SIPConfig .setUserName(ccEntry.userCredentials.getUserName()); } // encode and send what we have else if (ccEntry != null && (!ccEntryHasSeenTran || authHeader.isStale())) { } // if user canceled or sth else went wrong if (ccEntry.userCredentials == null) throw new SecurityException( "Unable to authenticate with realm " + realm); AuthorizationHeader authorization = this.getAuthorization( reoriginatedRequest.getMethod(), reoriginatedRequest .getRequestURI().toString(), reoriginatedRequest.getContent() == null ? "" : reoriginatedRequest.getContent().toString(), authHeader, ccEntry.userCredentials); ccEntry.processRequest(retryTran.getBranchId()); cachedCredentials.cacheEntry(realm, ccEntry); reoriginatedRequest.addHeader(authorization); // if there was trouble with the user - make sure we fix it if (uri.isSipURI()) { ((SipURI) uri).setUser(ccEntry.userCredentials .getUserName()); Address add = from.getAddress(); add.setURI(uri); from.setAddress(add); reoriginatedRequest.setHeader(from); if (challengedRequest.getMethod().equals(Request.REGISTER)) { ToHeader to = (ToHeader) reoriginatedRequest .getHeader(ToHeader.NAME); add.setURI(uri); to.setAddress(add); reoriginatedRequest.setHeader(to); } // very ugly but very necessary sipManCallback.setCurrentlyUsedURI(uri.toString()); Log.debug("URI: " + uri.toString()); } // if this is a register - fix to as well } return retryTran; } catch (Exception e) { Log.debug("ERRO REG: " + e.toString()); return null; } } /** * Generates an authorisation header in response to wwwAuthHeader. * * @param method method of the request being authenticated * @param uri digest-uri * @param authHeader the challenge that we should respond to * @param userCredentials username and pass * @return an authorisation header in response to wwwAuthHeader. */ private AuthorizationHeader getAuthorization(String method, String uri, String requestBody, WWWAuthenticateHeader authHeader, UserCredentials userCredentials) throws SecurityException { String response = null; try { Log.debug("getAuthorization", userCredentials.getAuthUserName()); response = MessageDigestAlgorithm.calculateResponse(authHeader .getAlgorithm(), userCredentials.getAuthUserName(), authHeader.getRealm(), new String(userCredentials .getPassword()), authHeader.getNonce(), // TODO we should one day implement those two null-s null,// nc-value null,// cnonce method, uri, requestBody, authHeader.getQop()); } catch (NullPointerException exc) { throw new SecurityException( "The authenticate header was malformatted"); } AuthorizationHeader authorization = null; try { if (authHeader instanceof ProxyAuthenticateHeader) { authorization = headerFactory .createProxyAuthorizationHeader(authHeader.getScheme()); } else { authorization = headerFactory .createAuthorizationHeader(authHeader.getScheme()); } authorization.setUsername(userCredentials.getAuthUserName()); authorization.setRealm(authHeader.getRealm()); authorization.setNonce(authHeader.getNonce()); authorization.setParameter("uri", uri); authorization.setResponse(response); if (authHeader.getAlgorithm() != null) authorization.setAlgorithm(authHeader.getAlgorithm()); if (authHeader.getOpaque() != null) authorization.setOpaque(authHeader.getOpaque()); authorization.setResponse(response); } catch (ParseException ex) { throw new SecurityException( "Failed to create an authorization header!"); } return authorization; } public void cacheCredentials(String realm, UserCredentials credentials) { CredentialsCacheEntry ccEntry = new CredentialsCacheEntry(); ccEntry.userCredentials = credentials; this.cachedCredentials.cacheEntry(realm, ccEntry); } /** * Sets a valid SipProvider that would enable the security manager to map * credentials to transactionsand thus understand when it is suitable to use * cached passwords and when it should go ask the user. * * @param transactionCreator a valid SipProvder instance * @uml.property name="transactionCreator" */ public void setTransactionCreator(SipProvider transactionCreator) { this.transactionCreator = transactionCreator; } /** * If the user name was wrong and the user fixes it here we should als * notify the sip manager that the currentlyUsedURI it has is not valid. * * @param sipManCallback a valid instance of SipMaqnager * @uml.property name="sipManCallback" */ public void setSipManCallback(SipManager sipManCallback) { this.sipManCallback = sipManCallback; } }