001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.auth;
029
030import java.io.IOException;
031import java.util.Locale;
032import java.util.Map;
033import java.util.Queue;
034
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.apache.http.Header;
038import org.apache.http.HttpException;
039import org.apache.http.HttpHost;
040import org.apache.http.HttpRequest;
041import org.apache.http.HttpResponse;
042import org.apache.http.auth.AuthOption;
043import org.apache.http.auth.AuthProtocolState;
044import org.apache.http.auth.AuthScheme;
045import org.apache.http.auth.AuthState;
046import org.apache.http.auth.AuthenticationException;
047import org.apache.http.auth.ContextAwareAuthScheme;
048import org.apache.http.auth.Credentials;
049import org.apache.http.auth.MalformedChallengeException;
050import org.apache.http.client.AuthenticationStrategy;
051import org.apache.http.protocol.HttpContext;
052import org.apache.http.util.Asserts;
053
054/**
055 * @since 4.3
056 */
057public class HttpAuthenticator {
058
059    private final Log log;
060
061    public HttpAuthenticator(final Log log) {
062        super();
063        this.log = log != null ? log : LogFactory.getLog(getClass());
064    }
065
066    public HttpAuthenticator() {
067        this(null);
068    }
069
070    public boolean isAuthenticationRequested(
071            final HttpHost host,
072            final HttpResponse response,
073            final AuthenticationStrategy authStrategy,
074            final AuthState authState,
075            final HttpContext context) {
076        if (authStrategy.isAuthenticationRequested(host, response, context)) {
077            this.log.debug("Authentication required");
078            if (authState.getState() == AuthProtocolState.SUCCESS) {
079                authStrategy.authFailed(host, authState.getAuthScheme(), context);
080            }
081            return true;
082        } else {
083            switch (authState.getState()) {
084            case CHALLENGED:
085            case HANDSHAKE:
086                this.log.debug("Authentication succeeded");
087                authState.setState(AuthProtocolState.SUCCESS);
088                authStrategy.authSucceeded(host, authState.getAuthScheme(), context);
089                break;
090            case SUCCESS:
091                break;
092            default:
093                authState.setState(AuthProtocolState.UNCHALLENGED);
094            }
095            return false;
096        }
097    }
098
099    public boolean handleAuthChallenge(
100            final HttpHost host,
101            final HttpResponse response,
102            final AuthenticationStrategy authStrategy,
103            final AuthState authState,
104            final HttpContext context) {
105        try {
106            if (this.log.isDebugEnabled()) {
107                this.log.debug(host.toHostString() + " requested authentication");
108            }
109            final Map<String, Header> challenges = authStrategy.getChallenges(host, response, context);
110            if (challenges.isEmpty()) {
111                this.log.debug("Response contains no authentication challenges");
112                return false;
113            }
114
115            final AuthScheme authScheme = authState.getAuthScheme();
116            switch (authState.getState()) {
117            case FAILURE:
118                return false;
119            case SUCCESS:
120                authState.reset();
121                break;
122            case CHALLENGED:
123            case HANDSHAKE:
124                if (authScheme == null) {
125                    this.log.debug("Auth scheme is null");
126                    authStrategy.authFailed(host, null, context);
127                    authState.reset();
128                    authState.setState(AuthProtocolState.FAILURE);
129                    return false;
130                }
131            case UNCHALLENGED:
132                if (authScheme != null) {
133                    final String id = authScheme.getSchemeName();
134                    final Header challenge = challenges.get(id.toLowerCase(Locale.ROOT));
135                    if (challenge != null) {
136                        this.log.debug("Authorization challenge processed");
137                        authScheme.processChallenge(challenge);
138                        if (authScheme.isComplete()) {
139                            this.log.debug("Authentication failed");
140                            authStrategy.authFailed(host, authState.getAuthScheme(), context);
141                            authState.reset();
142                            authState.setState(AuthProtocolState.FAILURE);
143                            return false;
144                        } else {
145                            authState.setState(AuthProtocolState.HANDSHAKE);
146                            return true;
147                        }
148                    } else {
149                        authState.reset();
150                        // Retry authentication with a different scheme
151                    }
152                }
153            }
154            final Queue<AuthOption> authOptions = authStrategy.select(challenges, host, response, context);
155            if (authOptions != null && !authOptions.isEmpty()) {
156                if (this.log.isDebugEnabled()) {
157                    this.log.debug("Selected authentication options: " + authOptions);
158                }
159                authState.setState(AuthProtocolState.CHALLENGED);
160                authState.update(authOptions);
161                return true;
162            } else {
163                return false;
164            }
165        } catch (final MalformedChallengeException ex) {
166            if (this.log.isWarnEnabled()) {
167                this.log.warn("Malformed challenge: " +  ex.getMessage());
168            }
169            authState.reset();
170            return false;
171        }
172    }
173
174    public void generateAuthResponse(
175            final HttpRequest request,
176            final AuthState authState,
177            final HttpContext context) throws HttpException, IOException {
178        AuthScheme authScheme = authState.getAuthScheme();
179        Credentials creds = authState.getCredentials();
180        switch (authState.getState()) { // TODO add UNCHALLENGED and HANDSHAKE cases
181        case FAILURE:
182            return;
183        case SUCCESS:
184            ensureAuthScheme(authScheme);
185            if (authScheme.isConnectionBased()) {
186                return;
187            }
188            break;
189        case CHALLENGED:
190            final Queue<AuthOption> authOptions = authState.getAuthOptions();
191            if (authOptions != null) {
192                while (!authOptions.isEmpty()) {
193                    final AuthOption authOption = authOptions.remove();
194                    authScheme = authOption.getAuthScheme();
195                    creds = authOption.getCredentials();
196                    authState.update(authScheme, creds);
197                    if (this.log.isDebugEnabled()) {
198                        this.log.debug("Generating response to an authentication challenge using "
199                                + authScheme.getSchemeName() + " scheme");
200                    }
201                    try {
202                        final Header header = doAuth(authScheme, creds, request, context);
203                        request.addHeader(header);
204                        break;
205                    } catch (final AuthenticationException ex) {
206                        if (this.log.isWarnEnabled()) {
207                            this.log.warn(authScheme + " authentication error: " + ex.getMessage());
208                        }
209                    }
210                }
211                return;
212            } else {
213                ensureAuthScheme(authScheme);
214            }
215        }
216        if (authScheme != null) {
217            try {
218                final Header header = doAuth(authScheme, creds, request, context);
219                request.addHeader(header);
220            } catch (final AuthenticationException ex) {
221                if (this.log.isErrorEnabled()) {
222                    this.log.error(authScheme + " authentication error: " + ex.getMessage());
223                }
224            }
225        }
226    }
227
228    private void ensureAuthScheme(final AuthScheme authScheme) {
229        Asserts.notNull(authScheme, "Auth scheme");
230    }
231
232    @SuppressWarnings("deprecation")
233    private Header doAuth(
234            final AuthScheme authScheme,
235            final Credentials creds,
236            final HttpRequest request,
237            final HttpContext context) throws AuthenticationException {
238        if (authScheme instanceof ContextAwareAuthScheme) {
239            return ((ContextAwareAuthScheme) authScheme).authenticate(creds, request, context);
240        } else {
241            return authScheme.authenticate(creds, request);
242        }
243    }
244
245}