001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.auth;
003
004import java.net.Authenticator;
005import java.net.PasswordAuthentication;
006import java.util.Collection;
007import java.util.HashSet;
008import java.util.Objects;
009
010import org.openstreetmap.josm.io.OsmApi;
011import org.openstreetmap.josm.tools.Logging;
012import org.openstreetmap.josm.tools.Pair;
013
014/**
015 * This is the default authenticator used in JOSM. It delegates lookup of credentials
016 * for the OSM API and an optional proxy server to the currently configured {@link CredentialsManager}.
017 * @since 2641
018 */
019public final class DefaultAuthenticator extends Authenticator {
020    private static final DefaultAuthenticator INSTANCE = new DefaultAuthenticator();
021
022    /**
023     * Returns the unique instance
024     * @return The unique instance
025     */
026    public static DefaultAuthenticator getInstance() {
027        return INSTANCE;
028    }
029
030    private final Collection<Pair<String, RequestorType>> failedCredentials = new HashSet<>();
031    private boolean enabled = true;
032
033    private DefaultAuthenticator() {
034    }
035
036    /**
037     * Called by the Java HTTP stack when either the OSM API server or a proxy requires authentication.
038     */
039    @Override
040    protected PasswordAuthentication getPasswordAuthentication() {
041        if (!enabled)
042            return null;
043        try {
044            if (OsmApi.isUsingOAuth()
045                    && Objects.equals(OsmApi.getOsmApi().getHost(), getRequestingHost())
046                    && RequestorType.SERVER.equals(getRequestorType())) {
047                // if we are working with OAuth we don't prompt for a password
048                return null;
049            }
050            final Pair<String, RequestorType> hostTypePair = Pair.create(getRequestingHost(), getRequestorType());
051            final boolean hasFailedPreviously = failedCredentials.contains(hostTypePair);
052            final CredentialsAgentResponse response = CredentialsManager.getInstance().getCredentials(
053                    getRequestorType(), getRequestingHost(), hasFailedPreviously);
054            if (response == null || response.isCanceled()) {
055                return null;
056            }
057            if (RequestorType.PROXY.equals(getRequestorType())) {
058                // Query user in case this authenticator is called (indicating that the authentication failed) the next time.
059                failedCredentials.add(hostTypePair);
060            } else {
061                // Other parallel requests should not ask the user again, thus wait till this request is finished.
062                // In case of invalid authentication, the host is added again to failedCredentials at HttpClient.connect()
063                failedCredentials.remove(hostTypePair);
064            }
065            return new PasswordAuthentication(response.getUsername(), response.getPassword());
066        } catch (CredentialsAgentException e) {
067            Logging.error(e);
068            return null;
069        }
070    }
071
072    /**
073     * Determines whether this authenticator is enabled, i.e.,
074     * provides {@link #getPasswordAuthentication() password authentication} via {@link CredentialsManager}.
075     * @return whether this authenticator is enabled
076     */
077    public boolean isEnabled() {
078        return enabled;
079    }
080
081    /**
082     * Enabled/disables this authenticator, i.e., decides whether it
083     * should provide {@link #getPasswordAuthentication() password authentication} via {@link CredentialsManager}.
084     * @param enabled whether this authenticator should be enabled
085     */
086    public void setEnabled(boolean enabled) {
087        this.enabled = enabled;
088    }
089
090    /**
091     * Marks for this host that the authentication failed, i.e.,
092     * the {@link CredentialsManager} will show a dialog at the next time.
093     * @param host the host to mark
094     * @return as per {@link Collection#add(Object)}
095     */
096    public boolean addFailedCredentialHost(String host) {
097        return failedCredentials.add(Pair.create(host, RequestorType.SERVER));
098    }
099
100    /**
101     * Un-marks the failed authentication attempt for the host
102     * @param host the host to un-mark
103     * @return as per {@link Collection#remove(Object)}
104     */
105    public boolean removeFailedCredentialHost(String host) {
106        return failedCredentials.remove(Pair.create(host, RequestorType.SERVER));
107    }
108}