source: josm/trunk/src/org/openstreetmap/josm/gui/oauth/OsmOAuthAuthorizationClient.java@ 5874

Last change on this file since 5874 was 5874, checked in by Don-vip, 11 years ago

see #8570, #7406 - I/O refactorization:

  • Move different file copy functions to Utils.copyFile (to be replaced later by Files.copy when switching to Java 7)
  • Replace all Utils.close(XXX) by Utils.close(Closeable) -> impacted plugins: commandline, mirrored_download, opendata, piclayer
  • Add new Utils.close(ZipFile)
  • Use of Utils.close almost everywhere
  • Javadoc fixes
  • Property svn:eol-style set to native
File size: 23.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.oauth;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedReader;
7import java.io.DataOutputStream;
8import java.io.IOException;
9import java.io.InputStreamReader;
10import java.io.UnsupportedEncodingException;
11import java.lang.reflect.Field;
12import java.net.HttpURLConnection;
13import java.net.MalformedURLException;
14import java.net.URL;
15import java.net.URLEncoder;
16import java.util.HashMap;
17import java.util.Iterator;
18import java.util.List;
19import java.util.Map;
20import java.util.Map.Entry;
21import java.util.regex.Matcher;
22import java.util.regex.Pattern;
23
24import oauth.signpost.OAuth;
25import oauth.signpost.OAuthConsumer;
26import oauth.signpost.OAuthProvider;
27import oauth.signpost.basic.DefaultOAuthProvider;
28import oauth.signpost.exception.OAuthCommunicationException;
29import oauth.signpost.exception.OAuthException;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.data.oauth.OAuthParameters;
33import org.openstreetmap.josm.data.oauth.OAuthToken;
34import org.openstreetmap.josm.data.oauth.OsmPrivileges;
35import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
36import org.openstreetmap.josm.gui.progress.ProgressMonitor;
37import org.openstreetmap.josm.io.OsmTransferCanceledException;
38import org.openstreetmap.josm.tools.CheckParameterUtil;
39import org.openstreetmap.josm.tools.Utils;
40
41/**
42 * An OAuth 1.0 authorization client.
43 * @since 2746
44 */
45public class OsmOAuthAuthorizationClient {
46 private final OAuthParameters oauthProviderParameters;
47 private final OAuthConsumer consumer;
48 private final OAuthProvider provider;
49 private boolean canceled;
50 private HttpURLConnection connection;
51
52 private static class SessionId {
53 String id;
54 String token;
55 String userName;
56 }
57
58 /**
59 * Creates a new authorisation client with default OAuth parameters
60 *
61 */
62 public OsmOAuthAuthorizationClient() {
63 oauthProviderParameters = OAuthParameters.createDefault(Main.pref.get("osm-server.url"));
64 consumer = oauthProviderParameters.buildConsumer();
65 provider = oauthProviderParameters.buildProvider(consumer);
66 }
67
68 /**
69 * Creates a new authorisation client with the parameters <code>parameters</code>.
70 *
71 * @param parameters the OAuth parameters. Must not be null.
72 * @throws IllegalArgumentException if parameters is null
73 */
74 public OsmOAuthAuthorizationClient(OAuthParameters parameters) throws IllegalArgumentException {
75 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
76 oauthProviderParameters = new OAuthParameters(parameters);
77 consumer = oauthProviderParameters.buildConsumer();
78 provider = oauthProviderParameters.buildProvider(consumer);
79 }
80
81 /**
82 * Creates a new authorisation client with the parameters <code>parameters</code>
83 * and an already known Request Token.
84 *
85 * @param parameters the OAuth parameters. Must not be null.
86 * @param requestToken the request token. Must not be null.
87 * @throws IllegalArgumentException if parameters is null
88 * @throws IllegalArgumentException if requestToken is null
89 */
90 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) throws IllegalArgumentException {
91 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
92 oauthProviderParameters = new OAuthParameters(parameters);
93 consumer = oauthProviderParameters.buildConsumer();
94 provider = oauthProviderParameters.buildProvider(consumer);
95 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret());
96 }
97
98 /**
99 * Cancels the current OAuth operation.
100 */
101 public void cancel() {
102 DefaultOAuthProvider p = (DefaultOAuthProvider)provider;
103 canceled = true;
104 if (p != null) {
105 try {
106 Field f = p.getClass().getDeclaredField("connection");
107 f.setAccessible(true);
108 HttpURLConnection con = (HttpURLConnection)f.get(p);
109 if (con != null) {
110 con.disconnect();
111 }
112 } catch(NoSuchFieldException e) {
113 e.printStackTrace();
114 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
115 } catch(SecurityException e) {
116 e.printStackTrace();
117 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
118 } catch(IllegalAccessException e) {
119 e.printStackTrace();
120 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
121 }
122 }
123 synchronized(this) {
124 if (connection != null) {
125 connection.disconnect();
126 }
127 }
128 }
129
130 /**
131 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service
132 * Provider and replies the request token.
133 *
134 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
135 * @return the OAuth Request Token
136 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
137 * @throws OsmTransferCanceledException if the user canceled the request
138 */
139 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
140 if (monitor == null) {
141 monitor = NullProgressMonitor.INSTANCE;
142 }
143 try {
144 monitor.beginTask("");
145 monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl()));
146 provider.retrieveRequestToken(consumer, "");
147 return OAuthToken.createToken(consumer);
148 } catch(OAuthCommunicationException e){
149 if (canceled)
150 throw new OsmTransferCanceledException();
151 throw new OsmOAuthAuthorizationException(e);
152 } catch(OAuthException e){
153 if (canceled)
154 throw new OsmTransferCanceledException();
155 throw new OsmOAuthAuthorizationException(e);
156 } finally {
157 monitor.finishTask();
158 }
159 }
160
161 /**
162 * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service
163 * Provider and replies the request token.
164 *
165 * You must have requested a Request Token using {@link #getRequestToken(ProgressMonitor)} first.
166 *
167 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
168 * @return the OAuth Access Token
169 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
170 * @throws OsmTransferCanceledException if the user canceled the request
171 * @see #getRequestToken(ProgressMonitor)
172 */
173 public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
174 if (monitor == null) {
175 monitor = NullProgressMonitor.INSTANCE;
176 }
177 try {
178 monitor.beginTask("");
179 monitor.indeterminateSubTask(tr("Retrieving OAuth Access Token from ''{0}''", oauthProviderParameters.getAccessTokenUrl()));
180 provider.retrieveAccessToken(consumer, null);
181 return OAuthToken.createToken(consumer);
182 } catch(OAuthCommunicationException e){
183 if (canceled)
184 throw new OsmTransferCanceledException();
185 throw new OsmOAuthAuthorizationException(e);
186 } catch(OAuthException e){
187 if (canceled)
188 throw new OsmTransferCanceledException();
189 throw new OsmOAuthAuthorizationException(e);
190 } finally {
191 monitor.finishTask();
192 }
193 }
194
195 /**
196 * Builds the authorise URL for a given Request Token. Users can be redirected to this URL.
197 * There they can login to OSM and authorise the request.
198 *
199 * @param requestToken the request token
200 * @return the authorise URL for this request
201 */
202 public String getAuthoriseUrl(OAuthToken requestToken) {
203 StringBuilder sb = new StringBuilder();
204
205 // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to
206 // the authorisation request, no callback parameter.
207 //
208 sb.append(oauthProviderParameters.getAuthoriseUrl()).append("?")
209 .append(OAuth.OAUTH_TOKEN).append("=").append(requestToken.getKey());
210 return sb.toString();
211 }
212
213 protected String extractToken(HttpURLConnection connection) {
214 try {
215 BufferedReader r = new BufferedReader(new InputStreamReader(connection.getInputStream()));
216 String c;
217 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*");
218 while((c = r.readLine()) != null) {
219 Matcher m = p.matcher(c);
220 if(m.find())
221 return m.group(1);
222 }
223 } catch (IOException e) {
224 return null;
225 }
226 return null;
227 }
228
229 protected SessionId extractOsmSession(HttpURLConnection connection) {
230 List<String> setCookies = connection.getHeaderFields().get("Set-Cookie");
231 if (setCookies == null)
232 // no cookies set
233 return null;
234
235 for (String setCookie: setCookies) {
236 String[] kvPairs = setCookie.split(";");
237 if (kvPairs == null || kvPairs.length == 0) {
238 continue;
239 }
240 for (String kvPair : kvPairs) {
241 kvPair = kvPair.trim();
242 String [] kv = kvPair.split("=");
243 if (kv == null || kv.length != 2) {
244 continue;
245 }
246 if (kv[0].equals("_osm_session")) {
247 // osm session cookie found
248 String token = extractToken(connection);
249 if(token == null)
250 return null;
251 SessionId si = new SessionId();
252 si.id = kv[1];
253 si.token = token;
254 return si;
255 }
256 }
257 }
258 return null;
259 }
260
261 protected String buildPostRequest(Map<String,String> parameters) throws OsmOAuthAuthorizationException {
262 try {
263 StringBuilder sb = new StringBuilder();
264
265 for(Iterator<Entry<String,String>> it = parameters.entrySet().iterator(); it.hasNext();) {
266 Entry<String,String> entry = it.next();
267 String value = entry.getValue();
268 value = (value == null) ? "" : value;
269 sb.append(entry.getKey()).append("=").append(URLEncoder.encode(value, "UTF-8"));
270 if (it.hasNext()) {
271 sb.append("&");
272 }
273 }
274 return sb.toString();
275 } catch(UnsupportedEncodingException e) {
276 throw new OsmOAuthAuthorizationException(e);
277 }
278 }
279
280 /**
281 * Derives the OSM login URL from the OAuth Authorization Website URL
282 *
283 * @return the OSM login URL
284 * @throws OsmOAuthAuthorizationException if something went wrong, in particular if the
285 * URLs are malformed
286 */
287 public String buildOsmLoginUrl() throws OsmOAuthAuthorizationException{
288 try {
289 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl());
290 URL url = new URL(Main.pref.get("oauth.protocol", "https"), autUrl.getHost(), autUrl.getPort(), "/login");
291 return url.toString();
292 } catch(MalformedURLException e) {
293 throw new OsmOAuthAuthorizationException(e);
294 }
295 }
296
297 /**
298 * Derives the OSM logout URL from the OAuth Authorization Website URL
299 *
300 * @return the OSM logout URL
301 * @throws OsmOAuthAuthorizationException if something went wrong, in particular if the
302 * URLs are malformed
303 */
304 protected String buildOsmLogoutUrl() throws OsmOAuthAuthorizationException{
305 try {
306 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl());
307 URL url = new URL("http", autUrl.getHost(), autUrl.getPort(), "/logout");
308 return url.toString();
309 } catch(MalformedURLException e) {
310 throw new OsmOAuthAuthorizationException(e);
311 }
312 }
313
314 /**
315 * Submits a request to the OSM website for a login form. The OSM website replies a session ID in
316 * a cookie.
317 *
318 * @return the session ID structure
319 * @throws OsmOAuthAuthorizationException if something went wrong
320 */
321 protected SessionId fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException {
322 try {
323 StringBuilder sb = new StringBuilder();
324 sb.append(buildOsmLoginUrl()).append("?cookie_test=true");
325 URL url = new URL(sb.toString());
326 synchronized(this) {
327 connection = Utils.openHttpConnection(url);
328 }
329 connection.setRequestMethod("GET");
330 connection.setDoInput(true);
331 connection.setDoOutput(false);
332 connection.connect();
333 SessionId sessionId = extractOsmSession(connection);
334 if (sessionId == null)
335 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
336 return sessionId;
337 } catch(IOException e) {
338 throw new OsmOAuthAuthorizationException(e);
339 } finally {
340 synchronized(this) {
341 connection = null;
342 }
343 }
344 }
345
346 /**
347 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in
348 * a hidden parameter.
349 *
350 * @throws OsmOAuthAuthorizationException if something went wrong
351 */
352 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException {
353 try {
354 URL url = new URL(getAuthoriseUrl(requestToken));
355 synchronized(this) {
356 connection = Utils.openHttpConnection(url);
357 }
358 connection.setRequestMethod("GET");
359 connection.setDoInput(true);
360 connection.setDoOutput(false);
361 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
362 connection.connect();
363 sessionId.token = extractToken(connection);
364 if (sessionId.token == null)
365 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
366 } catch(IOException e) {
367 throw new OsmOAuthAuthorizationException(e);
368 } finally {
369 synchronized(this) {
370 connection = null;
371 }
372 }
373 }
374
375 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException {
376 DataOutputStream dout = null;
377 try {
378 URL url = new URL(buildOsmLoginUrl());
379 synchronized(this) {
380 connection = Utils.openHttpConnection(url);
381 }
382 connection.setRequestMethod("POST");
383 connection.setDoInput(true);
384 connection.setDoOutput(true);
385 connection.setUseCaches(false);
386
387 Map<String,String> parameters = new HashMap<String, String>();
388 parameters.put("username", userName);
389 parameters.put("password", password);
390 parameters.put("referer", "/");
391 parameters.put("commit", "Login");
392 parameters.put("authenticity_token", sessionId.token);
393
394 String request = buildPostRequest(parameters);
395
396 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
397 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
398 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id);
399 // make sure we can catch 302 Moved Temporarily below
400 connection.setInstanceFollowRedirects(false);
401
402 connection.connect();
403
404 dout = new DataOutputStream(connection.getOutputStream());
405 dout.writeBytes(request);
406 dout.flush();
407 Utils.close(dout);
408
409 // after a successful login the OSM website sends a redirect to a follow up page. Everything
410 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with
411 // an error page is sent to back to the user.
412 //
413 int retCode = connection.getResponseCode();
414 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
415 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user", userName));
416 } catch(OsmOAuthAuthorizationException e) {
417 throw new OsmLoginFailedException(e.getCause());
418 } catch(IOException e) {
419 throw new OsmLoginFailedException(e);
420 } finally {
421 Utils.close(dout);
422 synchronized(this) {
423 connection = null;
424 }
425 }
426 }
427
428 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException {
429 try {
430 URL url = new URL(buildOsmLogoutUrl());
431 synchronized(this) {
432 connection = Utils.openHttpConnection(url);
433 }
434 connection.setRequestMethod("GET");
435 connection.setDoInput(true);
436 connection.setDoOutput(false);
437 connection.connect();
438 } catch(MalformedURLException e) {
439 throw new OsmOAuthAuthorizationException(e);
440 } catch(IOException e) {
441 throw new OsmOAuthAuthorizationException(e);
442 } finally {
443 synchronized(this) {
444 connection = null;
445 }
446 }
447 }
448
449 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges) throws OsmOAuthAuthorizationException {
450 Map<String, String> parameters = new HashMap<String, String>();
451 fetchOAuthToken(sessionId, requestToken);
452 parameters.put("oauth_token", requestToken.getKey());
453 parameters.put("oauth_callback", "");
454 parameters.put("authenticity_token", sessionId.token);
455 if (privileges.isAllowWriteApi()) {
456 parameters.put("allow_write_api", "yes");
457 }
458 if (privileges.isAllowWriteGpx()) {
459 parameters.put("allow_write_gpx", "yes");
460 }
461 if (privileges.isAllowReadGpx()) {
462 parameters.put("allow_read_gpx", "yes");
463 }
464 if (privileges.isAllowWritePrefs()) {
465 parameters.put("allow_write_prefs", "yes");
466 }
467 if (privileges.isAllowReadPrefs()) {
468 parameters.put("allow_read_prefs", "yes");
469 }
470
471 parameters.put("commit", "Save changes");
472
473 String request = buildPostRequest(parameters);
474 DataOutputStream dout = null;
475 try {
476 URL url = new URL(oauthProviderParameters.getAuthoriseUrl());
477 synchronized(this) {
478 connection = Utils.openHttpConnection(url);
479 }
480 connection.setRequestMethod("POST");
481 connection.setDoInput(true);
482 connection.setDoOutput(true);
483 connection.setUseCaches(false);
484 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
485 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
486 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
487 connection.setInstanceFollowRedirects(false);
488
489 connection.connect();
490
491 dout = new DataOutputStream(connection.getOutputStream());
492 dout.writeBytes(request);
493 dout.flush();
494
495 int retCode = connection.getResponseCode();
496 if (retCode != HttpURLConnection.HTTP_OK)
497 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey()));
498 } catch(MalformedURLException e) {
499 throw new OsmOAuthAuthorizationException(e);
500 } catch(IOException e) {
501 throw new OsmOAuthAuthorizationException(e);
502 } finally {
503 Utils.close(dout);
504 synchronized(this) {
505 connection = null;
506 }
507 }
508 }
509
510 /**
511 * Automatically authorises a request token for a set of privileges.
512 *
513 * @param requestToken the request token. Must not be null.
514 * @param osmUserName the OSM user name. Must not be null.
515 * @param osmPassword the OSM password. Must not be null.
516 * @param privileges the set of privileges. Must not be null.
517 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
518 * @throws IllegalArgumentException if requestToken is null
519 * @throws IllegalArgumentException if osmUserName is null
520 * @throws IllegalArgumentException if osmPassword is null
521 * @throws IllegalArgumentException if privileges is null
522 * @throws OsmOAuthAuthorizationException if the authorisation fails
523 * @throws OsmTransferCanceledException if the task is canceled by the user
524 */
525 public void authorise(OAuthToken requestToken, String osmUserName, String osmPassword, OsmPrivileges privileges, ProgressMonitor monitor) throws IllegalArgumentException, OsmOAuthAuthorizationException, OsmTransferCanceledException{
526 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken");
527 CheckParameterUtil.ensureParameterNotNull(osmUserName, "osmUserName");
528 CheckParameterUtil.ensureParameterNotNull(osmPassword, "osmPassword");
529 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges");
530
531 if (monitor == null) {
532 monitor = NullProgressMonitor.INSTANCE;
533 }
534 try {
535 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey()));
536 monitor.setTicksCount(4);
537 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website..."));
538 SessionId sessionId = fetchOsmWebsiteSessionId();
539 sessionId.userName = osmUserName;
540 if (canceled)
541 throw new OsmTransferCanceledException();
542 monitor.worked(1);
543
544 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", osmUserName));
545 authenticateOsmSession(sessionId, osmUserName, osmPassword);
546 if (canceled)
547 throw new OsmTransferCanceledException();
548 monitor.worked(1);
549
550 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey()));
551 sendAuthorisationRequest(sessionId, requestToken, privileges);
552 if (canceled)
553 throw new OsmTransferCanceledException();
554 monitor.worked(1);
555
556 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId));
557 logoutOsmSession(sessionId);
558 if (canceled)
559 throw new OsmTransferCanceledException();
560 monitor.worked(1);
561 } catch(OsmOAuthAuthorizationException e) {
562 if (canceled)
563 throw new OsmTransferCanceledException();
564 throw e;
565 } finally {
566 monitor.finishTask();
567 }
568 }
569}
Note: See TracBrowser for help on using the repository browser.