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