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