001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.lang.reflect.InvocationTargetException; 007import java.net.Authenticator.RequestorType; 008import java.net.MalformedURLException; 009import java.net.URL; 010import java.nio.charset.StandardCharsets; 011import java.util.Base64; 012import java.util.Objects; 013import java.util.concurrent.FutureTask; 014 015import javax.swing.SwingUtilities; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.data.oauth.OAuthParameters; 019import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard; 020import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder; 021import org.openstreetmap.josm.io.auth.CredentialsAgentException; 022import org.openstreetmap.josm.io.auth.CredentialsAgentResponse; 023import org.openstreetmap.josm.io.auth.CredentialsManager; 024import org.openstreetmap.josm.tools.HttpClient; 025import org.openstreetmap.josm.tools.Utils; 026 027import oauth.signpost.OAuthConsumer; 028import oauth.signpost.exception.OAuthException; 029 030/** 031 * Base class that handles common things like authentication for the reader and writer 032 * to the osm server. 033 * 034 * @author imi 035 */ 036public class OsmConnection { 037 protected boolean cancel; 038 protected HttpClient activeConnection; 039 protected OAuthParameters oauthParameters; 040 041 /** 042 * Cancels the connection. 043 */ 044 public void cancel() { 045 cancel = true; 046 synchronized (this) { 047 if (activeConnection != null) { 048 activeConnection.disconnect(); 049 } 050 } 051 } 052 053 /** 054 * Adds an authentication header for basic authentication 055 * 056 * @param con the connection 057 * @throws OsmTransferException if something went wrong. Check for nested exceptions 058 */ 059 protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException { 060 CredentialsAgentResponse response; 061 try { 062 synchronized (CredentialsManager.getInstance()) { 063 response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER, 064 con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */); 065 } 066 } catch (CredentialsAgentException e) { 067 throw new OsmTransferException(e); 068 } 069 String token; 070 if (response == null) { 071 token = ":"; 072 } else if (response.isCanceled()) { 073 cancel = true; 074 return; 075 } else { 076 String username = response.getUsername() == null ? "" : response.getUsername(); 077 String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword()); 078 token = username + ':' + password; 079 con.setHeader("Authorization", "Basic "+Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8))); 080 } 081 } 082 083 /** 084 * Signs the connection with an OAuth authentication header 085 * 086 * @param connection the connection 087 * 088 * @throws OsmTransferException if there is currently no OAuth Access Token configured 089 * @throws OsmTransferException if signing fails 090 */ 091 protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException { 092 if (oauthParameters == null) { 093 oauthParameters = OAuthParameters.createFromPreferences(Main.pref); 094 } 095 OAuthConsumer consumer = oauthParameters.buildConsumer(); 096 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance(); 097 if (!holder.containsAccessToken()) { 098 obtainAccessToken(connection); 099 } 100 if (!holder.containsAccessToken()) { // check if wizard completed 101 throw new MissingOAuthAccessTokenException(); 102 } 103 consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret()); 104 try { 105 consumer.sign(connection); 106 } catch (OAuthException e) { 107 throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e); 108 } 109 } 110 111 /** 112 * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}. 113 * @param connection connection for which the access token should be obtained 114 * @throws MissingOAuthAccessTokenException if the process cannot be completec successfully 115 */ 116 protected void obtainAccessToken(final HttpClient connection) throws MissingOAuthAccessTokenException { 117 try { 118 final URL apiUrl = new URL(OsmApi.getOsmApi().getServerUrl()); 119 if (!Objects.equals(apiUrl.getHost(), connection.getURL().getHost())) { 120 throw new MissingOAuthAccessTokenException(); 121 } 122 final Runnable authTask = new FutureTask<>(() -> { 123 // Concerning Utils.newDirectExecutor: Main.worker cannot be used since this connection is already 124 // executed via Main.worker. The OAuth connections would block otherwise. 125 final OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard( 126 Main.parent, apiUrl.toExternalForm(), Utils.newDirectExecutor()); 127 wizard.showDialog(); 128 OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true); 129 OAuthAccessTokenHolder.getInstance().save(Main.pref, CredentialsManager.getInstance()); 130 return wizard; 131 }); 132 // exception handling differs from implementation at GuiHelper.runInEDTAndWait() 133 if (SwingUtilities.isEventDispatchThread()) { 134 authTask.run(); 135 } else { 136 SwingUtilities.invokeAndWait(authTask); 137 } 138 } catch (MalformedURLException | InterruptedException | InvocationTargetException e) { 139 throw new MissingOAuthAccessTokenException(e); 140 } 141 } 142 143 protected void addAuth(HttpClient connection) throws OsmTransferException { 144 final String authMethod = OsmApi.getAuthMethod(); 145 if ("basic".equals(authMethod)) { 146 addBasicAuthorizationHeader(connection); 147 } else if ("oauth".equals(authMethod)) { 148 addOAuthAuthorizationHeader(connection); 149 } else { 150 String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod); 151 Main.warn(msg); 152 throw new OsmTransferException(msg); 153 } 154 } 155 156 /** 157 * Replies true if this connection is canceled 158 * 159 * @return true if this connection is canceled 160 */ 161 public boolean isCanceled() { 162 return cancel; 163 } 164}