1package 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
20import gov.nist.javax.sip.SipStackImpl;
21import gov.nist.javax.sip.address.SipUri;
22import gov.nist.javax.sip.message.SIPRequest;
23import gov.nist.javax.sip.stack.SIPClientTransaction;
24import gov.nist.javax.sip.stack.SIPTransactionStack;
25
26import java.text.ParseException;
27import java.util.Collection;
28import java.util.Iterator;
29import java.util.ListIterator;
30import java.util.Timer;
31
32import javax.sip.ClientTransaction;
33import javax.sip.DialogState;
34import javax.sip.InvalidArgumentException;
35import javax.sip.SipException;
36import javax.sip.SipProvider;
37import javax.sip.address.Hop;
38import javax.sip.address.SipURI;
39import javax.sip.address.URI;
40import javax.sip.header.AuthorizationHeader;
41import javax.sip.header.CSeqHeader;
42import javax.sip.header.Header;
43import javax.sip.header.HeaderFactory;
44import javax.sip.header.ProxyAuthenticateHeader;
45import javax.sip.header.ProxyAuthorizationHeader;
46import javax.sip.header.ViaHeader;
47import javax.sip.header.WWWAuthenticateHeader;
48import javax.sip.message.Request;
49import 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
63public 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