001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.oauth;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.io.IOException;
008import java.net.HttpURLConnection;
009import java.net.URL;
010
011import javax.swing.JOptionPane;
012import javax.xml.parsers.ParserConfigurationException;
013
014import org.openstreetmap.josm.data.oauth.OAuthParameters;
015import org.openstreetmap.josm.data.oauth.OAuthToken;
016import org.openstreetmap.josm.data.osm.UserInfo;
017import org.openstreetmap.josm.gui.HelpAwareOptionPane;
018import org.openstreetmap.josm.gui.PleaseWaitRunnable;
019import org.openstreetmap.josm.gui.help.HelpUtil;
020import org.openstreetmap.josm.io.OsmApiException;
021import org.openstreetmap.josm.io.OsmServerUserInfoReader;
022import org.openstreetmap.josm.io.OsmTransferException;
023import org.openstreetmap.josm.io.auth.DefaultAuthenticator;
024import org.openstreetmap.josm.tools.CheckParameterUtil;
025import org.openstreetmap.josm.tools.HttpClient;
026import org.openstreetmap.josm.tools.Logging;
027import org.openstreetmap.josm.tools.Utils;
028import org.openstreetmap.josm.tools.XmlParsingException;
029import org.openstreetmap.josm.tools.XmlUtils;
030import org.w3c.dom.Document;
031import org.xml.sax.SAXException;
032
033import oauth.signpost.OAuthConsumer;
034import oauth.signpost.exception.OAuthException;
035
036/**
037 * Checks whether an OSM API server can be accessed with a specific Access Token.
038 *
039 * It retrieves the user details for the user which is authorized to access the server with
040 * this token.
041 *
042 */
043public class TestAccessTokenTask extends PleaseWaitRunnable {
044    private final OAuthToken token;
045    private final OAuthParameters oauthParameters;
046    private boolean canceled;
047    private final Component parent;
048    private final String apiUrl;
049    private HttpClient connection;
050
051    /**
052     * Create the task
053     *
054     * @param parent the parent component relative to which the  {@link PleaseWaitRunnable}-Dialog is displayed
055     * @param apiUrl the API URL. Must not be null.
056     * @param parameters the OAuth parameters. Must not be null.
057     * @param accessToken the Access Token. Must not be null.
058     */
059    public TestAccessTokenTask(Component parent, String apiUrl, OAuthParameters parameters, OAuthToken accessToken) {
060        super(parent, tr("Testing OAuth Access Token"), false /* don't ignore exceptions */);
061        CheckParameterUtil.ensureParameterNotNull(apiUrl, "apiUrl");
062        CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
063        CheckParameterUtil.ensureParameterNotNull(accessToken, "accessToken");
064        this.token = accessToken;
065        this.oauthParameters = parameters;
066        this.parent = parent;
067        this.apiUrl = apiUrl;
068    }
069
070    @Override
071    protected void cancel() {
072        canceled = true;
073        synchronized (this) {
074            if (connection != null) {
075                connection.disconnect();
076            }
077        }
078    }
079
080    @Override
081    protected void finish() {
082        // Do nothing
083    }
084
085    protected void sign(HttpClient con) throws OAuthException {
086        OAuthConsumer consumer = oauthParameters.buildConsumer();
087        consumer.setTokenWithSecret(token.getKey(), token.getSecret());
088        consumer.sign(con);
089    }
090
091    protected String normalizeApiUrl(String url) {
092        // remove leading and trailing white space
093        url = url.trim();
094
095        // remove trailing slashes
096        while (url.endsWith("/")) {
097            url = url.substring(0, url.lastIndexOf('/'));
098        }
099        return url;
100    }
101
102    protected UserInfo getUserDetails() throws OsmOAuthAuthorizationException, XmlParsingException, OsmTransferException {
103        boolean authenticatorEnabled = true;
104        try {
105            URL url = new URL(normalizeApiUrl(apiUrl) + "/0.6/user/details");
106            authenticatorEnabled = DefaultAuthenticator.getInstance().isEnabled();
107            DefaultAuthenticator.getInstance().setEnabled(false);
108
109            final HttpClient client = HttpClient.create(url);
110            sign(client);
111            synchronized (this) {
112                connection = client;
113                connection.connect();
114            }
115
116            if (connection.getResponse().getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED)
117                throw new OsmApiException(HttpURLConnection.HTTP_UNAUTHORIZED,
118                        tr("Retrieving user details with Access Token Key ''{0}'' was rejected.", token.getKey()), null);
119
120            if (connection.getResponse().getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN)
121                throw new OsmApiException(HttpURLConnection.HTTP_FORBIDDEN,
122                        tr("Retrieving user details with Access Token Key ''{0}'' was forbidden.", token.getKey()), null);
123
124            if (connection.getResponse().getResponseCode() != HttpURLConnection.HTTP_OK)
125                throw new OsmApiException(connection.getResponse().getResponseCode(),
126                        connection.getResponse().getHeaderField("Error"), null);
127            Document d = XmlUtils.parseSafeDOM(connection.getResponse().getContent());
128            return OsmServerUserInfoReader.buildFromXML(d);
129        } catch (SAXException | ParserConfigurationException e) {
130            throw new XmlParsingException(e);
131        } catch (IOException e) {
132            throw new OsmTransferException(e);
133        } catch (OAuthException e) {
134            throw new OsmOAuthAuthorizationException(e);
135        } finally {
136            DefaultAuthenticator.getInstance().setEnabled(authenticatorEnabled);
137        }
138    }
139
140    protected void notifySuccess(UserInfo userInfo) {
141        HelpAwareOptionPane.showMessageDialogInEDT(
142                parent,
143                tr("<html>"
144                        + "Successfully used the Access Token ''{0}'' to<br>"
145                        + "access the OSM server at ''{1}''.<br>"
146                        + "You are accessing the OSM server as user ''{2}'' with id ''{3}''."
147                        + "</html>",
148                        token.getKey(),
149                        apiUrl,
150                        Utils.escapeReservedCharactersHTML(userInfo.getDisplayName()),
151                        userInfo.getId()
152                ),
153                tr("Success"),
154                JOptionPane.INFORMATION_MESSAGE,
155                HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenOK")
156        );
157    }
158
159    protected void alertFailedAuthentication() {
160        HelpAwareOptionPane.showMessageDialogInEDT(
161                parent,
162                tr("<html>"
163                        + "Failed to access the OSM server ''{0}''<br>"
164                        + "with the Access Token ''{1}''.<br>"
165                        + "The server rejected the Access Token as unauthorized. You will not<br>"
166                        + "be able to access any protected resource on this server using this token."
167                        +"</html>",
168                        apiUrl,
169                        token.getKey()
170                ),
171                tr("Test failed"),
172                JOptionPane.ERROR_MESSAGE,
173                HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
174        );
175    }
176
177    protected void alertFailedAuthorisation() {
178        HelpAwareOptionPane.showMessageDialogInEDT(
179                parent,
180                tr("<html>"
181                        + "The Access Token ''{1}'' is known to the OSM server ''{0}''.<br>"
182                        + "The test to retrieve the user details for this token failed, though.<br>"
183                        + "Depending on what rights are granted to this token you may nevertheless use it<br>"
184                        + "to upload data, upload GPS traces, and/or access other protected resources."
185                        +"</html>",
186                        apiUrl,
187                        token.getKey()
188                ),
189                tr("Token allows restricted access"),
190                JOptionPane.WARNING_MESSAGE,
191                HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
192        );
193    }
194
195    protected void alertFailedConnection() {
196        HelpAwareOptionPane.showMessageDialogInEDT(
197                parent,
198                tr("<html>"
199                        + "Failed to retrieve information about the current user"
200                        + " from the OSM server ''{0}''.<br>"
201                        + "This is probably not a problem caused by the tested Access Token, but<br>"
202                        + "rather a problem with the server configuration. Carefully check the server<br>"
203                        + "URL and your Internet connection."
204                        +"</html>",
205                        apiUrl,
206                        token.getKey()
207                ),
208                tr("Test failed"),
209                JOptionPane.ERROR_MESSAGE,
210                HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
211        );
212    }
213
214    protected void alertFailedSigning() {
215        HelpAwareOptionPane.showMessageDialogInEDT(
216                parent,
217                tr("<html>"
218                        + "Failed to sign the request for the OSM server ''{0}'' with the "
219                        + "token ''{1}''.<br>"
220                        + "The token ist probably invalid."
221                        +"</html>",
222                        apiUrl,
223                        token.getKey()
224                ),
225                tr("Test failed"),
226                JOptionPane.ERROR_MESSAGE,
227                HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
228        );
229    }
230
231    protected void alertInternalError() {
232        HelpAwareOptionPane.showMessageDialogInEDT(
233                parent,
234                tr("<html>"
235                        + "The test failed because the server responded with an internal error.<br>"
236                        + "JOSM could not decide whether the token is valid. Please try again later."
237                        + "</html>",
238                        apiUrl,
239                        token.getKey()
240                ),
241                tr("Test failed"),
242                JOptionPane.WARNING_MESSAGE,
243                HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
244        );
245    }
246
247    @Override
248    protected void realRun() throws SAXException, IOException, OsmTransferException {
249        try {
250            getProgressMonitor().indeterminateSubTask(tr("Retrieving user info..."));
251            UserInfo userInfo = getUserDetails();
252            if (canceled) return;
253            notifySuccess(userInfo);
254        } catch (OsmOAuthAuthorizationException e) {
255            if (canceled) return;
256            Logging.error(e);
257            alertFailedSigning();
258        } catch (OsmApiException e) {
259            if (canceled) return;
260            Logging.error(e);
261            if (e.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) {
262                alertInternalError();
263                return;
264            } else if (e.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
265                alertFailedAuthentication();
266                return;
267            } else if (e.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) {
268                alertFailedAuthorisation();
269                return;
270            }
271            alertFailedConnection();
272        } catch (OsmTransferException e) {
273            if (canceled) return;
274            Logging.error(e);
275            alertFailedConnection();
276        }
277    }
278}