001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.lang.reflect.InvocationTargetException;
007import java.net.Authenticator.RequestorType;
008import java.net.MalformedURLException;
009import java.net.URL;
010import java.nio.charset.StandardCharsets;
011import java.util.Base64;
012import java.util.Objects;
013import java.util.concurrent.FutureTask;
014
015import javax.swing.SwingUtilities;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.oauth.OAuthParameters;
019import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
020import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
021import org.openstreetmap.josm.io.auth.CredentialsAgentException;
022import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
023import org.openstreetmap.josm.io.auth.CredentialsManager;
024import org.openstreetmap.josm.tools.HttpClient;
025import org.openstreetmap.josm.tools.Utils;
026
027import oauth.signpost.OAuthConsumer;
028import oauth.signpost.exception.OAuthException;
029
030/**
031 * Base class that handles common things like authentication for the reader and writer
032 * to the osm server.
033 *
034 * @author imi
035 */
036public class OsmConnection {
037    protected boolean cancel;
038    protected HttpClient activeConnection;
039    protected OAuthParameters oauthParameters;
040
041    /**
042     * Cancels the connection.
043     */
044    public void cancel() {
045        cancel = true;
046        synchronized (this) {
047            if (activeConnection != null) {
048                activeConnection.disconnect();
049            }
050        }
051    }
052
053    /**
054     * Adds an authentication header for basic authentication
055     *
056     * @param con the connection
057     * @throws OsmTransferException if something went wrong. Check for nested exceptions
058     */
059    protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException {
060        CredentialsAgentResponse response;
061        try {
062            synchronized (CredentialsManager.getInstance()) {
063                response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
064                con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
065            }
066        } catch (CredentialsAgentException e) {
067            throw new OsmTransferException(e);
068        }
069        String token;
070        if (response == null) {
071            token = ":";
072        } else if (response.isCanceled()) {
073            cancel = true;
074            return;
075        } else {
076            String username = response.getUsername() == null ? "" : response.getUsername();
077            String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword());
078            token = username + ':' + password;
079            con.setHeader("Authorization", "Basic "+Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8)));
080        }
081    }
082
083    /**
084     * Signs the connection with an OAuth authentication header
085     *
086     * @param connection the connection
087     *
088     * @throws OsmTransferException if there is currently no OAuth Access Token configured
089     * @throws OsmTransferException if signing fails
090     */
091    protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException {
092        if (oauthParameters == null) {
093            oauthParameters = OAuthParameters.createFromPreferences(Main.pref);
094        }
095        OAuthConsumer consumer = oauthParameters.buildConsumer();
096        OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
097        if (!holder.containsAccessToken()) {
098            obtainAccessToken(connection);
099        }
100        if (!holder.containsAccessToken()) { // check if wizard completed
101            throw new MissingOAuthAccessTokenException();
102        }
103        consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret());
104        try {
105            consumer.sign(connection);
106        } catch (OAuthException e) {
107            throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
108        }
109    }
110
111    /**
112     * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
113     * @param connection connection for which the access token should be obtained
114     * @throws MissingOAuthAccessTokenException if the process cannot be completec successfully
115     */
116    protected void obtainAccessToken(final HttpClient connection) throws MissingOAuthAccessTokenException {
117        try {
118            final URL apiUrl = new URL(OsmApi.getOsmApi().getServerUrl());
119            if (!Objects.equals(apiUrl.getHost(), connection.getURL().getHost())) {
120                throw new MissingOAuthAccessTokenException();
121            }
122            final Runnable authTask = new FutureTask<>(() -> {
123                // Concerning Utils.newDirectExecutor: Main.worker cannot be used since this connection is already
124                // executed via Main.worker. The OAuth connections would block otherwise.
125                final OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
126                        Main.parent, apiUrl.toExternalForm(), Utils.newDirectExecutor());
127                wizard.showDialog();
128                OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true);
129                OAuthAccessTokenHolder.getInstance().save(Main.pref, CredentialsManager.getInstance());
130                return wizard;
131            });
132            // exception handling differs from implementation at GuiHelper.runInEDTAndWait()
133            if (SwingUtilities.isEventDispatchThread()) {
134                authTask.run();
135            } else {
136                SwingUtilities.invokeAndWait(authTask);
137            }
138        } catch (MalformedURLException | InterruptedException | InvocationTargetException e) {
139            throw new MissingOAuthAccessTokenException(e);
140        }
141    }
142
143    protected void addAuth(HttpClient connection) throws OsmTransferException {
144        final String authMethod = OsmApi.getAuthMethod();
145        if ("basic".equals(authMethod)) {
146            addBasicAuthorizationHeader(connection);
147        } else if ("oauth".equals(authMethod)) {
148            addOAuthAuthorizationHeader(connection);
149        } else {
150            String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
151            Main.warn(msg);
152            throw new OsmTransferException(msg);
153        }
154    }
155
156    /**
157     * Replies true if this connection is canceled
158     *
159     * @return true if this connection is canceled
160     */
161    public boolean isCanceled() {
162        return cancel;
163    }
164}