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.io.BufferedReader; 007import java.io.DataOutputStream; 008import java.io.IOException; 009import java.io.InputStreamReader; 010import java.io.UnsupportedEncodingException; 011import java.lang.reflect.Field; 012import java.net.HttpURLConnection; 013import java.net.MalformedURLException; 014import java.net.URL; 015import java.net.URLEncoder; 016import java.util.HashMap; 017import java.util.Iterator; 018import java.util.List; 019import java.util.Map; 020import java.util.Map.Entry; 021import java.util.regex.Matcher; 022import java.util.regex.Pattern; 023 024import oauth.signpost.OAuth; 025import oauth.signpost.OAuthConsumer; 026import oauth.signpost.OAuthProvider; 027import oauth.signpost.basic.DefaultOAuthProvider; 028import oauth.signpost.exception.OAuthCommunicationException; 029import oauth.signpost.exception.OAuthException; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.data.oauth.OAuthParameters; 033import org.openstreetmap.josm.data.oauth.OAuthToken; 034import org.openstreetmap.josm.data.oauth.OsmPrivileges; 035import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 036import org.openstreetmap.josm.gui.progress.ProgressMonitor; 037import org.openstreetmap.josm.io.OsmTransferCanceledException; 038import org.openstreetmap.josm.tools.CheckParameterUtil; 039import org.openstreetmap.josm.tools.Utils; 040 041/** 042 * An OAuth 1.0 authorization client. 043 * @since 2746 044 */ 045public class OsmOAuthAuthorizationClient { 046 private final OAuthParameters oauthProviderParameters; 047 private final OAuthConsumer consumer; 048 private final OAuthProvider provider; 049 private boolean canceled; 050 private HttpURLConnection connection; 051 052 private static class SessionId { 053 String id; 054 String token; 055 String userName; 056 } 057 058 /** 059 * Creates a new authorisation client with default OAuth parameters 060 * 061 */ 062 public OsmOAuthAuthorizationClient() { 063 oauthProviderParameters = OAuthParameters.createDefault(Main.pref.get("osm-server.url")); 064 consumer = oauthProviderParameters.buildConsumer(); 065 provider = oauthProviderParameters.buildProvider(consumer); 066 } 067 068 /** 069 * Creates a new authorisation client with the parameters <code>parameters</code>. 070 * 071 * @param parameters the OAuth parameters. Must not be null. 072 * @throws IllegalArgumentException if parameters is null 073 */ 074 public OsmOAuthAuthorizationClient(OAuthParameters parameters) throws IllegalArgumentException { 075 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); 076 oauthProviderParameters = new OAuthParameters(parameters); 077 consumer = oauthProviderParameters.buildConsumer(); 078 provider = oauthProviderParameters.buildProvider(consumer); 079 } 080 081 /** 082 * Creates a new authorisation client with the parameters <code>parameters</code> 083 * and an already known Request Token. 084 * 085 * @param parameters the OAuth parameters. Must not be null. 086 * @param requestToken the request token. Must not be null. 087 * @throws IllegalArgumentException if parameters is null 088 * @throws IllegalArgumentException if requestToken is null 089 */ 090 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) throws IllegalArgumentException { 091 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); 092 oauthProviderParameters = new OAuthParameters(parameters); 093 consumer = oauthProviderParameters.buildConsumer(); 094 provider = oauthProviderParameters.buildProvider(consumer); 095 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret()); 096 } 097 098 /** 099 * 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 Main.warn(tr("Failed to cancel running OAuth operation")); 115 } catch (SecurityException e) { 116 e.printStackTrace(); 117 Main.warn(tr("Failed to cancel running OAuth operation")); 118 } catch (IllegalAccessException e) { 119 e.printStackTrace(); 120 Main.warn(tr("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 BufferedReader r = null; 215 try { 216 r = new BufferedReader(new InputStreamReader(connection.getInputStream())); 217 String c; 218 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*"); 219 while ((c = r.readLine()) != null) { 220 Matcher m = p.matcher(c); 221 if (m.find()) { 222 return m.group(1); 223 } 224 } 225 } catch (IOException e) { 226 Main.error(e); 227 return null; 228 } finally { 229 Utils.close(r); 230 } 231 return null; 232 } 233 234 protected SessionId extractOsmSession(HttpURLConnection connection) { 235 List<String> setCookies = connection.getHeaderFields().get("Set-Cookie"); 236 if (setCookies == null) 237 // no cookies set 238 return null; 239 240 for (String setCookie: setCookies) { 241 String[] kvPairs = setCookie.split(";"); 242 if (kvPairs == null || kvPairs.length == 0) { 243 continue; 244 } 245 for (String kvPair : kvPairs) { 246 kvPair = kvPair.trim(); 247 String [] kv = kvPair.split("="); 248 if (kv == null || kv.length != 2) { 249 continue; 250 } 251 if (kv[0].equals("_osm_session")) { 252 // osm session cookie found 253 String token = extractToken(connection); 254 if(token == null) 255 return null; 256 SessionId si = new SessionId(); 257 si.id = kv[1]; 258 si.token = token; 259 return si; 260 } 261 } 262 } 263 return null; 264 } 265 266 protected String buildPostRequest(Map<String,String> parameters) throws OsmOAuthAuthorizationException { 267 try { 268 StringBuilder sb = new StringBuilder(); 269 270 for(Iterator<Entry<String,String>> it = parameters.entrySet().iterator(); it.hasNext();) { 271 Entry<String,String> entry = it.next(); 272 String value = entry.getValue(); 273 value = (value == null) ? "" : value; 274 sb.append(entry.getKey()).append("=").append(URLEncoder.encode(value, "UTF-8")); 275 if (it.hasNext()) { 276 sb.append("&"); 277 } 278 } 279 return sb.toString(); 280 } catch(UnsupportedEncodingException e) { 281 throw new OsmOAuthAuthorizationException(e); 282 } 283 } 284 285 /** 286 * Derives the OSM login URL from the OAuth Authorization Website URL 287 * 288 * @return the OSM login URL 289 * @throws OsmOAuthAuthorizationException if something went wrong, in particular if the 290 * URLs are malformed 291 */ 292 public String buildOsmLoginUrl() throws OsmOAuthAuthorizationException{ 293 try { 294 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl()); 295 URL url = new URL(Main.pref.get("oauth.protocol", "https"), autUrl.getHost(), autUrl.getPort(), "/login"); 296 return url.toString(); 297 } catch(MalformedURLException e) { 298 throw new OsmOAuthAuthorizationException(e); 299 } 300 } 301 302 /** 303 * Derives the OSM logout URL from the OAuth Authorization Website URL 304 * 305 * @return the OSM logout URL 306 * @throws OsmOAuthAuthorizationException if something went wrong, in particular if the 307 * URLs are malformed 308 */ 309 protected String buildOsmLogoutUrl() throws OsmOAuthAuthorizationException{ 310 try { 311 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl()); 312 URL url = new URL("http", autUrl.getHost(), autUrl.getPort(), "/logout"); 313 return url.toString(); 314 } catch(MalformedURLException e) { 315 throw new OsmOAuthAuthorizationException(e); 316 } 317 } 318 319 /** 320 * Submits a request to the OSM website for a login form. The OSM website replies a session ID in 321 * a cookie. 322 * 323 * @return the session ID structure 324 * @throws OsmOAuthAuthorizationException if something went wrong 325 */ 326 protected SessionId fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException { 327 try { 328 StringBuilder sb = new StringBuilder(); 329 sb.append(buildOsmLoginUrl()).append("?cookie_test=true"); 330 URL url = new URL(sb.toString()); 331 synchronized(this) { 332 connection = Utils.openHttpConnection(url); 333 } 334 connection.setRequestMethod("GET"); 335 connection.setDoInput(true); 336 connection.setDoOutput(false); 337 connection.connect(); 338 SessionId sessionId = extractOsmSession(connection); 339 if (sessionId == null) 340 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString())); 341 return sessionId; 342 } catch(IOException e) { 343 throw new OsmOAuthAuthorizationException(e); 344 } finally { 345 synchronized(this) { 346 connection = null; 347 } 348 } 349 } 350 351 /** 352 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in 353 * a hidden parameter. 354 * 355 * @throws OsmOAuthAuthorizationException if something went wrong 356 */ 357 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException { 358 try { 359 URL url = new URL(getAuthoriseUrl(requestToken)); 360 synchronized(this) { 361 connection = Utils.openHttpConnection(url); 362 } 363 connection.setRequestMethod("GET"); 364 connection.setDoInput(true); 365 connection.setDoOutput(false); 366 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName); 367 connection.connect(); 368 sessionId.token = extractToken(connection); 369 if (sessionId.token == null) 370 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString())); 371 } catch(IOException e) { 372 throw new OsmOAuthAuthorizationException(e); 373 } finally { 374 synchronized(this) { 375 connection = null; 376 } 377 } 378 } 379 380 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException { 381 DataOutputStream dout = null; 382 try { 383 URL url = new URL(buildOsmLoginUrl()); 384 synchronized(this) { 385 connection = Utils.openHttpConnection(url); 386 } 387 connection.setRequestMethod("POST"); 388 connection.setDoInput(true); 389 connection.setDoOutput(true); 390 connection.setUseCaches(false); 391 392 Map<String,String> parameters = new HashMap<String, String>(); 393 parameters.put("username", userName); 394 parameters.put("password", password); 395 parameters.put("referer", "/"); 396 parameters.put("commit", "Login"); 397 parameters.put("authenticity_token", sessionId.token); 398 399 String request = buildPostRequest(parameters); 400 401 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 402 connection.setRequestProperty("Content-Length", Integer.toString(request.length())); 403 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id); 404 // make sure we can catch 302 Moved Temporarily below 405 connection.setInstanceFollowRedirects(false); 406 407 connection.connect(); 408 409 dout = new DataOutputStream(connection.getOutputStream()); 410 dout.writeBytes(request); 411 dout.flush(); 412 Utils.close(dout); 413 414 // after a successful login the OSM website sends a redirect to a follow up page. Everything 415 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with 416 // an error page is sent to back to the user. 417 // 418 int retCode = connection.getResponseCode(); 419 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP) 420 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user", userName)); 421 } catch(OsmOAuthAuthorizationException e) { 422 throw new OsmLoginFailedException(e.getCause()); 423 } catch(IOException e) { 424 throw new OsmLoginFailedException(e); 425 } finally { 426 Utils.close(dout); 427 synchronized(this) { 428 connection = null; 429 } 430 } 431 } 432 433 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException { 434 try { 435 URL url = new URL(buildOsmLogoutUrl()); 436 synchronized(this) { 437 connection = Utils.openHttpConnection(url); 438 } 439 connection.setRequestMethod("GET"); 440 connection.setDoInput(true); 441 connection.setDoOutput(false); 442 connection.connect(); 443 } catch(MalformedURLException e) { 444 throw new OsmOAuthAuthorizationException(e); 445 } catch(IOException e) { 446 throw new OsmOAuthAuthorizationException(e); 447 } finally { 448 synchronized(this) { 449 connection = null; 450 } 451 } 452 } 453 454 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges) throws OsmOAuthAuthorizationException { 455 Map<String, String> parameters = new HashMap<String, String>(); 456 fetchOAuthToken(sessionId, requestToken); 457 parameters.put("oauth_token", requestToken.getKey()); 458 parameters.put("oauth_callback", ""); 459 parameters.put("authenticity_token", sessionId.token); 460 if (privileges.isAllowWriteApi()) { 461 parameters.put("allow_write_api", "yes"); 462 } 463 if (privileges.isAllowWriteGpx()) { 464 parameters.put("allow_write_gpx", "yes"); 465 } 466 if (privileges.isAllowReadGpx()) { 467 parameters.put("allow_read_gpx", "yes"); 468 } 469 if (privileges.isAllowWritePrefs()) { 470 parameters.put("allow_write_prefs", "yes"); 471 } 472 if (privileges.isAllowReadPrefs()) { 473 parameters.put("allow_read_prefs", "yes"); 474 } 475 if(privileges.isAllowModifyNotes()) { 476 parameters.put("allow_write_notes", "yes"); 477 } 478 479 parameters.put("commit", "Save changes"); 480 481 String request = buildPostRequest(parameters); 482 DataOutputStream dout = null; 483 try { 484 URL url = new URL(oauthProviderParameters.getAuthoriseUrl()); 485 synchronized(this) { 486 connection = Utils.openHttpConnection(url); 487 } 488 connection.setRequestMethod("POST"); 489 connection.setDoInput(true); 490 connection.setDoOutput(true); 491 connection.setUseCaches(false); 492 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 493 connection.setRequestProperty("Content-Length", Integer.toString(request.length())); 494 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName); 495 connection.setInstanceFollowRedirects(false); 496 497 connection.connect(); 498 499 dout = new DataOutputStream(connection.getOutputStream()); 500 dout.writeBytes(request); 501 dout.flush(); 502 503 int retCode = connection.getResponseCode(); 504 if (retCode != HttpURLConnection.HTTP_OK) 505 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey())); 506 } catch(MalformedURLException e) { 507 throw new OsmOAuthAuthorizationException(e); 508 } catch(IOException e) { 509 throw new OsmOAuthAuthorizationException(e); 510 } finally { 511 Utils.close(dout); 512 synchronized(this) { 513 connection = null; 514 } 515 } 516 } 517 518 /** 519 * Automatically authorises a request token for a set of privileges. 520 * 521 * @param requestToken the request token. Must not be null. 522 * @param osmUserName the OSM user name. Must not be null. 523 * @param osmPassword the OSM password. Must not be null. 524 * @param privileges the set of privileges. Must not be null. 525 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 526 * @throws IllegalArgumentException if requestToken is null 527 * @throws IllegalArgumentException if osmUserName is null 528 * @throws IllegalArgumentException if osmPassword is null 529 * @throws IllegalArgumentException if privileges is null 530 * @throws OsmOAuthAuthorizationException if the authorisation fails 531 * @throws OsmTransferCanceledException if the task is canceled by the user 532 */ 533 public void authorise(OAuthToken requestToken, String osmUserName, String osmPassword, OsmPrivileges privileges, ProgressMonitor monitor) throws IllegalArgumentException, OsmOAuthAuthorizationException, OsmTransferCanceledException{ 534 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken"); 535 CheckParameterUtil.ensureParameterNotNull(osmUserName, "osmUserName"); 536 CheckParameterUtil.ensureParameterNotNull(osmPassword, "osmPassword"); 537 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges"); 538 539 if (monitor == null) { 540 monitor = NullProgressMonitor.INSTANCE; 541 } 542 try { 543 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey())); 544 monitor.setTicksCount(4); 545 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website...")); 546 SessionId sessionId = fetchOsmWebsiteSessionId(); 547 sessionId.userName = osmUserName; 548 if (canceled) 549 throw new OsmTransferCanceledException(); 550 monitor.worked(1); 551 552 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", osmUserName)); 553 authenticateOsmSession(sessionId, osmUserName, osmPassword); 554 if (canceled) 555 throw new OsmTransferCanceledException(); 556 monitor.worked(1); 557 558 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey())); 559 sendAuthorisationRequest(sessionId, requestToken, privileges); 560 if (canceled) 561 throw new OsmTransferCanceledException(); 562 monitor.worked(1); 563 564 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId)); 565 logoutOsmSession(sessionId); 566 if (canceled) 567 throw new OsmTransferCanceledException(); 568 monitor.worked(1); 569 } catch(OsmOAuthAuthorizationException e) { 570 if (canceled) 571 throw new OsmTransferCanceledException(); 572 throw e; 573 } finally { 574 monitor.finishTask(); 575 } 576 } 577}