1 package gov.nist.javax.sip.clientauthutils; 2 3 /* 4 * 5 * This code has been contributed with permission from: 6 * 7 * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client but has been significantly changed. 8 * It is donated to the JAIN-SIP project as it is common code that many sip clients 9 * need to perform class and others will consitute a set of utility functions 10 * that will implement common operations that ease the life of the developer. 11 * 12 * Acknowledgements: 13 * ---------------- 14 * 15 * Fredrik Wickstrom reported that dialog cseq counters are not incremented 16 * when resending requests. He later uncovered additional problems and 17 * proposed a way to fix them (his proposition was taken into account). 18 */ 19 20 import gov.nist.javax.sip.SipStackImpl; 21 import gov.nist.javax.sip.address.SipUri; 22 import gov.nist.javax.sip.message.SIPRequest; 23 import gov.nist.javax.sip.stack.SIPClientTransaction; 24 import gov.nist.javax.sip.stack.SIPTransactionStack; 25 26 import java.text.ParseException; 27 import java.util.Collection; 28 import java.util.Iterator; 29 import java.util.ListIterator; 30 import java.util.Timer; 31 32 import javax.sip.ClientTransaction; 33 import javax.sip.DialogState; 34 import javax.sip.InvalidArgumentException; 35 import javax.sip.SipException; 36 import javax.sip.SipProvider; 37 import javax.sip.address.Hop; 38 import javax.sip.address.SipURI; 39 import javax.sip.address.URI; 40 import javax.sip.header.AuthorizationHeader; 41 import javax.sip.header.CSeqHeader; 42 import javax.sip.header.Header; 43 import javax.sip.header.HeaderFactory; 44 import javax.sip.header.ProxyAuthenticateHeader; 45 import javax.sip.header.ProxyAuthorizationHeader; 46 import javax.sip.header.ViaHeader; 47 import javax.sip.header.WWWAuthenticateHeader; 48 import javax.sip.message.Request; 49 import javax.sip.message.Response; 50 51 /** 52 * The class handles authentication challenges, caches user credentials and takes care (through 53 * the SecurityAuthority interface) about retrieving passwords. 54 * 55 * 56 * @author Emil Ivov 57 * @author Jeroen van Bemmel 58 * @author M. Ranganathan 59 * 60 * @since 2.0 61 */ 62 63 public class AuthenticationHelperImpl implements AuthenticationHelper { 64 65 /** 66 * Credentials cached so far. 67 */ 68 private CredentialsCache cachedCredentials; 69 70 /** 71 * The account manager for the system. Stores user credentials. 72 */ 73 private Object accountManager = null; 74 75 /* 76 * Header factory for this security manager. 77 */ 78 private HeaderFactory headerFactory; 79 80 private SipStackImpl sipStack; 81 82 Timer timer; 83 84 /** 85 * Default constructor for the security manager. There is one Account manager. There is one 86 * SipSecurity manager for every user name, 87 * 88 * @param sipStack -- our stack. 89 * @param accountManager -- an implementation of the AccountManager interface. 90 * @param headerFactory -- header factory. 91 */ 92 public AuthenticationHelperImpl(SipStackImpl sipStack, AccountManager accountManager, 93 HeaderFactory headerFactory) { 94 this.accountManager = accountManager; 95 this.headerFactory = headerFactory; 96 this.sipStack = sipStack; 97 98 this.cachedCredentials = new CredentialsCache(((SIPTransactionStack) sipStack).getTimer()); 99 } 100 101 /** 102 * Default constructor for the security manager. There is one Account manager. There is one 103 * SipSecurity manager for every user name, 104 * 105 * @param sipStack -- our stack. 106 * @param accountManager -- an implementation of the AccountManager interface. 107 * @param headerFactory -- header factory. 108 */ 109 public AuthenticationHelperImpl(SipStackImpl sipStack, SecureAccountManager accountManager, 110 HeaderFactory headerFactory) { 111 this.accountManager = accountManager; 112 this.headerFactory = headerFactory; 113 this.sipStack = sipStack; 114 115 this.cachedCredentials = new CredentialsCache(((SIPTransactionStack) sipStack).getTimer()); 116 } 117 118 119 /* 120 * (non-Javadoc) 121 * 122 * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#handleChallenge(javax.sip.message.Response, 123 * javax.sip.ClientTransaction, javax.sip.SipProvider) 124 */ 125 public ClientTransaction handleChallenge(Response challenge, 126 ClientTransaction challengedTransaction, SipProvider transactionCreator, int cacheTime) 127 throws SipException, NullPointerException { 128 try { 129 if (sipStack.isLoggingEnabled()) { 130 sipStack.getStackLogger().logDebug("handleChallenge: " + challenge); 131 } 132 133 SIPRequest challengedRequest = ((SIPRequest) challengedTransaction.getRequest()); 134 135 Request reoriginatedRequest = null; 136 /* 137 * If the challenged request is part of a Dialog and the 138 * Dialog is confirmed the re-originated request should be 139 * generated as an in-Dialog request. 140 */ 141 if ( challengedRequest.getToTag() != null || 142 challengedTransaction.getDialog() == null || 143 challengedTransaction.getDialog().getState() != DialogState.CONFIRMED) { 144 reoriginatedRequest = (Request) challengedRequest.clone(); 145 } else { 146 /* 147 * Re-originate the request by consulting the dialog. In particular 148 * the route set could change between the original request and the 149 * in-dialog challenge. 150 */ 151 reoriginatedRequest = 152 challengedTransaction.getDialog().createRequest(challengedRequest.getMethod()); 153 Iterator<String> headerNames = challengedRequest.getHeaderNames(); 154 while (headerNames.hasNext()) { 155 String headerName = headerNames.next(); 156 if ( reoriginatedRequest.getHeader(headerName) != null) { 157 ListIterator<Header> iterator = reoriginatedRequest.getHeaders(headerName); 158 while (iterator.hasNext()) { 159 reoriginatedRequest.addHeader(iterator.next()); 160 } 161 } 162 } 163 } 164 165 166 167 // remove the branch id so that we could use the request in a new 168 // transaction 169 removeBranchID(reoriginatedRequest); 170 171 if (challenge == null || reoriginatedRequest == null) { 172 throw new NullPointerException("A null argument was passed to handle challenge."); 173 } 174 175 ListIterator authHeaders = null; 176 177 if (challenge.getStatusCode() == Response.UNAUTHORIZED) { 178 authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME); 179 } else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED) { 180 authHeaders = challenge.getHeaders(ProxyAuthenticateHeader.NAME); 181 } else { 182 throw new IllegalArgumentException("Unexpected status code "); 183 } 184 185 if (authHeaders == null) { 186 throw new IllegalArgumentException( 187 "Could not find WWWAuthenticate or ProxyAuthenticate headers"); 188 } 189 190 // Remove all authorization headers from the request (we'll re-add them 191 // from cache) 192 reoriginatedRequest.removeHeader(AuthorizationHeader.NAME); 193 reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME); 194 195 // rfc 3261 says that the cseq header should be augmented for the new 196 // request. do it here so that the new dialog (created together with 197 // the new client transaction) takes it into account. 198 // Bug report - Fredrik Wickstrom 199 CSeqHeader cSeq = (CSeqHeader) reoriginatedRequest.getHeader((CSeqHeader.NAME)); 200 try { 201 cSeq.setSeqNumber(cSeq.getSeqNumber() + 1l); 202 } catch (InvalidArgumentException ex) { 203 throw new SipException("Invalid CSeq -- could not increment : " 204 + cSeq.getSeqNumber()); 205 } 206 207 208 /* Resolve this to the next hop based on the previous lookup. If we are not using 209 * lose routing (RFC2543) then just attach hop as a maddr param. 210 */ 211 if ( challengedRequest.getRouteHeaders() == null ) { 212 Hop hop = ((SIPClientTransaction) challengedTransaction).getNextHop(); 213 SipURI sipUri = (SipURI) reoriginatedRequest.getRequestURI(); 214 // BEGIN android-added 215 if ( !hop.getHost().equalsIgnoreCase(sipUri.getHost()) 216 && !hop.equals(sipStack.getRouter(challengedRequest).getOutboundProxy()) ) 217 // END android-added 218 sipUri.setMAddrParam(hop.getHost()); 219 if ( hop.getPort() != -1 ) sipUri.setPort(hop.getPort()); 220 } 221 ClientTransaction retryTran = transactionCreator 222 .getNewClientTransaction(reoriginatedRequest); 223 224 WWWAuthenticateHeader authHeader = null; 225 SipURI requestUri = (SipURI) challengedTransaction.getRequest().getRequestURI(); 226 while (authHeaders.hasNext()) { 227 authHeader = (WWWAuthenticateHeader) authHeaders.next(); 228 String realm = authHeader.getRealm(); 229 AuthorizationHeader authorization = null; 230 String sipDomain; 231 if ( this.accountManager instanceof SecureAccountManager ) { 232 UserCredentialHash credHash = 233 ((SecureAccountManager)this.accountManager).getCredentialHash(challengedTransaction,realm); 234 URI uri = reoriginatedRequest.getRequestURI(); 235 sipDomain = credHash.getSipDomain(); 236 authorization = this.getAuthorization(reoriginatedRequest 237 .getMethod(), uri.toString(), 238 (reoriginatedRequest.getContent() == null) ? "" : new String( 239 reoriginatedRequest.getRawContent()), authHeader, credHash); 240 } else { 241 UserCredentials userCreds = ((AccountManager) this.accountManager).getCredentials(challengedTransaction, realm); 242 sipDomain = userCreds.getSipDomain(); 243 if (userCreds == null) 244 throw new SipException( 245 "Cannot find user creds for the given user name and realm"); 246 247 // we haven't yet authenticated this realm since we were 248 // started. 249 250 authorization = this.getAuthorization(reoriginatedRequest 251 .getMethod(), reoriginatedRequest.getRequestURI().toString(), 252 (reoriginatedRequest.getContent() == null) ? "" : new String( 253 reoriginatedRequest.getRawContent()), authHeader, userCreds); 254 } 255 if (sipStack.isLoggingEnabled()) 256 sipStack.getStackLogger().logDebug( 257 "Created authorization header: " + authorization.toString()); 258 259 if (cacheTime != 0) 260 cachedCredentials.cacheAuthorizationHeader(sipDomain, 261 authorization, cacheTime); 262 263 reoriginatedRequest.addHeader(authorization); 264 } 265 266 if (sipStack.isLoggingEnabled()) { 267 sipStack.getStackLogger().logDebug( 268 "Returning authorization transaction." + retryTran); 269 } 270 return retryTran; 271 } catch (SipException ex) { 272 throw ex; 273 } catch (Exception ex) { 274 sipStack.getStackLogger().logError("Unexpected exception ", ex); 275 throw new SipException("Unexpected exception ", ex); 276 } 277 } 278 279 280 281 282 /** 283 * Generates an authorisation header in response to wwwAuthHeader. 284 * 285 * @param method method of the request being authenticated 286 * @param uri digest-uri 287 * @param requestBody the body of the request. 288 * @param authHeader the challenge that we should respond to 289 * @param userCredentials username and pass 290 * 291 * @return an authorisation header in response to authHeader. 292 * 293 * @throws OperationFailedException if auth header was malformated. 294 */ 295 private AuthorizationHeader getAuthorization(String method, String uri, String requestBody, 296 WWWAuthenticateHeader authHeader, UserCredentials userCredentials) { 297 String response = null; 298 299 // JvB: authHeader.getQop() is a quoted _list_ of qop values 300 // (e.g. "auth,auth-int") Client is supposed to pick one 301 String qopList = authHeader.getQop(); 302 String qop = (qopList != null) ? "auth" : null; 303 String nc_value = "00000001"; 304 String cnonce = "xyz"; 305 306 response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(), 307 userCredentials.getUserName(), authHeader.getRealm(), userCredentials 308 .getPassword(), authHeader.getNonce(), nc_value, // JvB added 309 cnonce, // JvB added 310 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed 311 312 AuthorizationHeader authorization = null; 313 try { 314 if (authHeader instanceof ProxyAuthenticateHeader) { 315 authorization = headerFactory.createProxyAuthorizationHeader(authHeader 316 .getScheme()); 317 } else { 318 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme()); 319 } 320 321 authorization.setUsername(userCredentials.getUserName()); 322 authorization.setRealm(authHeader.getRealm()); 323 authorization.setNonce(authHeader.getNonce()); 324 authorization.setParameter("uri", uri); 325 authorization.setResponse(response); 326 if (authHeader.getAlgorithm() != null) { 327 authorization.setAlgorithm(authHeader.getAlgorithm()); 328 } 329 330 if (authHeader.getOpaque() != null) { 331 authorization.setOpaque(authHeader.getOpaque()); 332 } 333 334 // jvb added 335 if (qop != null) { 336 authorization.setQop(qop); 337 authorization.setCNonce(cnonce); 338 authorization.setNonceCount(Integer.parseInt(nc_value)); 339 } 340 341 authorization.setResponse(response); 342 343 } catch (ParseException ex) { 344 throw new RuntimeException("Failed to create an authorization header!"); 345 } 346 347 return authorization; 348 } 349 /** 350 * Generates an authorisation header in response to wwwAuthHeader. 351 * 352 * @param method method of the request being authenticated 353 * @param uri digest-uri 354 * @param requestBody the body of the request. 355 * @param authHeader the challenge that we should respond to 356 * @param userCredentials username and pass 357 * 358 * @return an authorisation header in response to authHeader. 359 * 360 * @throws OperationFailedException if auth header was malformated. 361 */ 362 private AuthorizationHeader getAuthorization(String method, String uri, String requestBody, 363 WWWAuthenticateHeader authHeader, UserCredentialHash userCredentials) { 364 String response = null; 365 366 // JvB: authHeader.getQop() is a quoted _list_ of qop values 367 // (e.g. "auth,auth-int") Client is supposed to pick one 368 String qopList = authHeader.getQop(); 369 String qop = (qopList != null) ? "auth" : null; 370 String nc_value = "00000001"; 371 String cnonce = "xyz"; 372 373 response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(), 374 userCredentials.getHashUserDomainPassword(), authHeader.getNonce(), nc_value, // JvB added 375 cnonce, // JvB added 376 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed 377 378 AuthorizationHeader authorization = null; 379 try { 380 if (authHeader instanceof ProxyAuthenticateHeader) { 381 authorization = headerFactory.createProxyAuthorizationHeader(authHeader 382 .getScheme()); 383 } else { 384 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme()); 385 } 386 387 authorization.setUsername(userCredentials.getUserName()); 388 authorization.setRealm(authHeader.getRealm()); 389 authorization.setNonce(authHeader.getNonce()); 390 authorization.setParameter("uri", uri); 391 authorization.setResponse(response); 392 if (authHeader.getAlgorithm() != null) { 393 authorization.setAlgorithm(authHeader.getAlgorithm()); 394 } 395 396 if (authHeader.getOpaque() != null) { 397 authorization.setOpaque(authHeader.getOpaque()); 398 } 399 400 // jvb added 401 if (qop != null) { 402 authorization.setQop(qop); 403 authorization.setCNonce(cnonce); 404 authorization.setNonceCount(Integer.parseInt(nc_value)); 405 } 406 407 authorization.setResponse(response); 408 409 } catch (ParseException ex) { 410 throw new RuntimeException("Failed to create an authorization header!"); 411 } 412 413 return authorization; 414 } 415 /** 416 * Removes all via headers from <tt>request</tt> and replaces them with a new one, equal to 417 * the one that was top most. 418 * 419 * @param request the Request whose branchID we'd like to remove. 420 * 421 */ 422 private void removeBranchID(Request request) { 423 424 ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); 425 426 viaHeader.removeParameter("branch"); 427 428 } 429 430 /* 431 * (non-Javadoc) 432 * 433 * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#attachAuthenticationHeaders(javax.sip.message.Request) 434 */ 435 public void setAuthenticationHeaders(Request request) { 436 SIPRequest sipRequest = (SIPRequest) request; 437 438 String callId = sipRequest.getCallId().getCallId(); 439 440 request.removeHeader(AuthorizationHeader.NAME); 441 Collection<AuthorizationHeader> authHeaders = this.cachedCredentials 442 .getCachedAuthorizationHeaders(callId); 443 if (authHeaders == null) { 444 if (sipStack.isLoggingEnabled()) 445 sipStack.getStackLogger().logDebug( 446 "Could not find authentication headers for " + callId); 447 return; 448 } 449 450 for (AuthorizationHeader authHeader : authHeaders) { 451 request.addHeader(authHeader); 452 } 453 454 } 455 456 /* 457 * (non-Javadoc) 458 * 459 * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#removeCachedAuthenticationHeaders(java.lang.String) 460 */ 461 public void removeCachedAuthenticationHeaders(String callId) { 462 if (callId == null) 463 throw new NullPointerException("Null callId argument "); 464 this.cachedCredentials.removeAuthenticationHeader(callId); 465 466 } 467 468 } 469