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