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