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