<commit_before>/*
 * Copyright 2015 <NAME>
 *
 * 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 io.reinert.requestor.auth;

import javax.annotation.Nullable;

import com.google.gwt.core.client.Duration;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.i18n.client.NumberFormat;

import io.reinert.requestor.HttpConnection;
import io.reinert.requestor.Payload;
import io.reinert.requestor.RawResponse;
import io.reinert.requestor.RequestException;
import io.reinert.requestor.RequestOrder;
import io.reinert.requestor.RequestProgress;
import io.reinert.requestor.Response;
import io.reinert.requestor.UnsuccessfulResponseException;
import io.reinert.requestor.deferred.Promise;
import io.reinert.requestor.uri.UriParser;

/**
 * HTTP Digest Authentication implementation. <br/>
 * This class supports MD5 digest authentication based on <a href="http://tools.ietf.org/html/rfc2617">RFC 2617</a>.
 *
 * @author <NAME>
 */
public class DigestAuth implements Authentication {

    /**
     * An array containing the codes which are considered expected for the first attempt to retrieve the nonce.
     *
     * The default expected code is unauthorized response 401.
     * 404 is put here because some might prevent the browser from prompting the user for the credentials,
     * which will commonly happen if the server returns with a 401 code.
     */
    public static int[] EXPECTED_CODES = new int[]{ 401, 404 };

    /**
     * The default number of max attempts for authenticating using one {@link DigestAuth} instance.
     *
     * It's normally two because the first attempt fails and returns the info for performing the authentication next.
     */
    public static int DEFAULT_MAX_CHALLENGE_CALLS = 2;

    private static NumberFormat NC_FORMAT = NumberFormat.getFormat("#00000000");

    private final String user;
    private final String password;
    private final boolean withCredentials;

    private String uri;
    private String httpMethod;

    private int maxChallengeCalls = DEFAULT_MAX_CHALLENGE_CALLS;
    private int challengeCalls = 1;
    private int nonceCount;
    private String lastNonce;

    public DigestAuth(String user, String password) {
        this(user, password, false);
    }

    public DigestAuth(String user, String password, boolean withCredentials) {
        this.user = user;
        this.password = password;
        this.withCredentials = withCredentials;
    }

    @Override
    public void authenticate(final RequestOrder request) {
        request.setWithCredentials(withCredentials);
        attempt(request, null);
    }

    /**
     * Set the max number of attempts to authenticate.
     * The default is 2.
     *
     * @param maxChallengeCalls  max number of attempt calls
     */
    public void setMaxChallengeCalls(int maxChallengeCalls) {
        this.maxChallengeCalls = maxChallengeCalls;
    }

    private void attempt(RequestOrder originalRequest, @Nullable Response attemptResponse) {
        if (challengeCalls < maxChallengeCalls) {
            final RequestOrder attemptRequest = getAttemptRequest(originalRequest);
            applyDigest(attemptRequest, attemptResponse);
            attemptRequest.send();
        } else {
            applyDigest(originalRequest, attemptResponse);
            originalRequest.send();
        }
        challengeCalls++;
    }

    private RequestOrder getAttemptRequest(final RequestOrder originalRequest) {
        return originalRequest.copy(RawResponse.class, new Deferred<RawResponse>() {
            @Override
            public void resolve(Response<RawResponse> response) {
                // If the attempt succeeded, then abort the original request with the successful response
                originalRequest.abort(response.getPayload());
            }

            @Override
            public void reject(RequestException error) {
                try {
                    if (error instanceof UnsuccessfulResponseException) {
                        UnsuccessfulResponseException e = (UnsuccessfulResponseException) error;
                        if (contains(EXPECTED_CODES, e.getStatusCode())) {
                            // If the error response code is expected, then continue trying to authenticate
                            attempt(originalRequest, e.getResponse());
                            return;
                        }
                    }
                    // Otherwise, throw an AuthenticationException and reject the promise with it
                    throw new AuthenticationException("Unable to authenticate using Digest: The server returned other "
                            + "response than one from expected.", error);
                } catch (Exception e) {
                    originalRequest.abort(new RequestException("Request could not be completed. See previous log.", e));
                }
            }
        });
    }

    private void applyDigest(RequestOrder request, Response attemptResponse) {
        if (attemptResponse == null)
            return;

        final String authHeader = attemptResponse.getHeader("WWW-Authenticate");
        if (authHeader == null) {
            throw new AuthenticationException("Unable to authenticate using Digest: It was not possible to retrieve "
                    + "the 'WWW-Authenticate' header from server response. If you're using CORS, make sure your "
                    + "server allows the client to access this header by adding the header "
                    + "\"Access-Control-Expose-Headers: WWW-Authenticate\" to the response.");
        }

        final StringBuilder digestBuilder = new StringBuilder("Digest username=\"").append(user);

        if (uri == null) {
            uri = UriParser.newInstance().parse(request.getUrl()).getUri().getPath();
            httpMethod = request.getMethod().getValue();
        }

        final String realm = readRealm(authHeader);
        final String opaque = readOpaque(authHeader);
        final String nonce = readNonce(authHeader);
        final String[] qop = readQop(authHeader);

        final String nc = getNonceCount(nonce);
        final String cNonce = generateClientNonce(nonce, nc);

        digestBuilder.append("\", realm=\"").append(realm);
        digestBuilder.append("\", nonce=\"").append(nonce);
        digestBuilder.append("\", uri=\"").append(uri);<commit_msg>DigestAuth exception messages improvement.<commit_after>/*
 * Copyright 2015 <NAME>
 *
 * 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 io.reinert.requestor.auth;

import javax.annotation.Nullable;

import com.google.gwt.core.client.Duration;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.i18n.client.NumberFormat;

import io.reinert.requestor.HttpConnection;
import io.reinert.requestor.Payload;
import io.reinert.requestor.RawResponse;
import io.reinert.requestor.RequestException;
import io.reinert.requestor.RequestOrder;
import io.reinert.requestor.RequestProgress;
import io.reinert.requestor.Response;
import io.reinert.requestor.UnsuccessfulResponseException;
import io.reinert.requestor.deferred.Promise;
import io.reinert.requestor.uri.UriParser;

/**
 * HTTP Digest Authentication implementation. <br/>
 * This class supports MD5 digest authentication based on <a href="http://tools.ietf.org/html/rfc2617">RFC 2617</a>.
 *
 * @author <NAME>
 */
public class DigestAuth implements Authentication {

    /**
     * An array containing the codes which are considered expected for the first attempt to retrieve the nonce.
     *
     * The default expected code is unauthorized response 401.
     * 404 is put here because some might prevent the browser from prompting the user for the credentials,
     * which will commonly happen if the server returns with a 401 code.
     */
    public static int[] EXPECTED_CODES = new int[]{ 401, 404 };

    /**
     * The default number of max attempts for authenticating using one {@link DigestAuth} instance.
     *
     * It's normally two because the first attempt fails and returns the info for performing the authentication next.
     */
    public static int DEFAULT_MAX_CHALLENGE_CALLS = 2;

    private static NumberFormat NC_FORMAT = NumberFormat.getFormat("#00000000");

    private final String user;
    private final String password;
    private final boolean withCredentials;

    private String uri;
    private String httpMethod;

    private int maxChallengeCalls = DEFAULT_MAX_CHALLENGE_CALLS;
    private int challengeCalls = 1;
    private int nonceCount;
    private String lastNonce;

    public DigestAuth(String user, String password) {
        this(user, password, false);
    }

    public DigestAuth(String user, String password, boolean withCredentials) {
        this.user = user;
        this.password = password;
        this.withCredentials = withCredentials;
    }

    @Override
    public void authenticate(final RequestOrder request) {
        request.setWithCredentials(withCredentials);
        attempt(request, null);
    }

    /**
     * Set the max number of attempts to authenticate.
     * The default is 2.
     *
     * @param maxChallengeCalls  max number of attempt calls
     */
    public void setMaxChallengeCalls(int maxChallengeCalls) {
        this.maxChallengeCalls = maxChallengeCalls;
    }

    private void attempt(RequestOrder originalRequest, @Nullable Response attemptResponse) {
        if (challengeCalls < maxChallengeCalls) {
            final RequestOrder attemptRequest = getAttemptRequest(originalRequest);
            applyDigest(attemptRequest, attemptResponse);
            attemptRequest.send();
        } else {
            applyDigest(originalRequest, attemptResponse);
            originalRequest.send();
        }
        challengeCalls++;
    }

    private RequestOrder getAttemptRequest(final RequestOrder originalRequest) {
        return originalRequest.copy(RawResponse.class, new Deferred<RawResponse>() {
            @Override
            public void resolve(Response<RawResponse> response) {
                // If the attempt succeeded, then abort the original request with the successful response
                originalRequest.abort(response.getPayload());
            }

            @Override
            public void reject(RequestException error) {
                try {
                    if (error instanceof UnsuccessfulResponseException) {
                        UnsuccessfulResponseException e = (UnsuccessfulResponseException) error;
                        if (contains(EXPECTED_CODES, e.getStatusCode())) {
                            // If the error response code is expected, then continue trying to authenticate
                            attempt(originalRequest, e.getResponse());
                            return;
                        }
                    }
                    // Otherwise, throw an AuthenticationException and reject the promise with it
                    throw new AuthenticationException("The server returned a not expected status code.", error);
                } catch (Exception e) {
                    originalRequest.abort(new RequestException("Unable to authenticate request using DigestAuth. "
                            + "See previous log.", e));
                }
            }
        });
    }

    private void applyDigest(RequestOrder request, Response attemptResponse) {
        if (attemptResponse == null)
            return;

        final String authHeader = attemptResponse.getHeader("WWW-Authenticate");
        if (authHeader == null) {
            throw new AuthenticationException("It was not possible to retrieve the 'WWW-Authenticate' header from "
                    + "server response. If you're using CORS, make sure your server allows the client to access this "
                    + "header by adding \"Access-Control-Expose-Headers: WWW-Authenticate\" to the response headers.");
        }

        final StringBuilder digestBuilder = new StringBuilder("Digest username=\"").append(user);

        if (uri == null) {
            uri = UriParser.newInstance().parse(request.getUrl()).getUri().getPath();
            httpMethod = request.getMethod().getValue();
        }

        final String realm = readRealm(authHeader);
        final String opaque = readOpaque(authHeader);
        final String nonce = readNonce(authHeader);
        final String[] qop = readQop(authHeader);

        final String nc = getNonceCount(nonce);
        final String cNonce = generateClientNonce(nonce, nc);

        digestBuilder.append("\", realm=\"").append(realm);
        digestBuilder.append("\", nonce=\"").append(nonce);
        digestBuilder.append("\", uri=\"").append(uri);
{'fraction_non_alphanumeric': 0.08200189684103013, 'fraction_numerical': 0.006565988181221274, 'mean_word_length': 2.6691648822269807, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1698602', 'n_tokens_mistral': 3507, 'n_tokens_neox': 3324, 'n_words': 1091}