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