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; 013 014import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder; 015import org.openstreetmap.josm.data.oauth.OAuthParameters; 016import org.openstreetmap.josm.io.auth.CredentialsAgentException; 017import org.openstreetmap.josm.io.auth.CredentialsAgentResponse; 018import org.openstreetmap.josm.io.auth.CredentialsManager; 019import org.openstreetmap.josm.tools.HttpClient; 020import org.openstreetmap.josm.tools.JosmRuntimeException; 021import org.openstreetmap.josm.tools.Logging; 022 023import oauth.signpost.OAuthConsumer; 024import oauth.signpost.exception.OAuthException; 025 026/** 027 * Base class that handles common things like authentication for the reader and writer 028 * to the osm server. 029 * 030 * @author imi 031 */ 032public class OsmConnection { 033 034 private static final String BASIC_AUTH = "Basic "; 035 036 protected boolean cancel; 037 protected HttpClient activeConnection; 038 protected OAuthParameters oauthParameters; 039 040 /** 041 * Retrieves OAuth access token. 042 * @since 12803 043 */ 044 public interface OAuthAccessTokenFetcher { 045 /** 046 * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}. 047 * @param serverUrl the URL to OSM server 048 * @throws InterruptedException if we're interrupted while waiting for the event dispatching thread to finish OAuth authorization task 049 * @throws InvocationTargetException if an exception is thrown while running OAuth authorization task 050 */ 051 void obtainAccessToken(URL serverUrl) throws InvocationTargetException, InterruptedException; 052 } 053 054 static volatile OAuthAccessTokenFetcher fetcher = u -> { 055 throw new JosmRuntimeException("OsmConnection.setOAuthAccessTokenFetcher() has not been called"); 056 }; 057 058 /** 059 * Sets the OAuth access token fetcher. 060 * @param tokenFetcher new OAuth access token fetcher. Cannot be null 061 * @since 12803 062 */ 063 public static void setOAuthAccessTokenFetcher(OAuthAccessTokenFetcher tokenFetcher) { 064 fetcher = Objects.requireNonNull(tokenFetcher, "tokenFetcher"); 065 } 066 067 /** 068 * Cancels the connection. 069 */ 070 public void cancel() { 071 cancel = true; 072 synchronized (this) { 073 if (activeConnection != null) { 074 activeConnection.disconnect(); 075 } 076 } 077 } 078 079 /** 080 * Retrieves login from basic authentication header, if set. 081 * 082 * @param con the connection 083 * @return login from basic authentication header, or {@code null} 084 * @throws OsmTransferException if something went wrong. Check for nested exceptions 085 * @since 12992 086 */ 087 protected String retrieveBasicAuthorizationLogin(HttpClient con) throws OsmTransferException { 088 String auth = con.getRequestHeader("Authorization"); 089 if (auth != null && auth.startsWith(BASIC_AUTH)) { 090 try { 091 String[] token = new String(Base64.getDecoder().decode(auth.substring(BASIC_AUTH.length())), 092 StandardCharsets.UTF_8).split(":"); 093 if (token.length == 2) { 094 return token[0]; 095 } 096 } catch (IllegalArgumentException e) { 097 Logging.error(e); 098 } 099 } 100 return null; 101 } 102 103 /** 104 * Adds an authentication header for basic authentication 105 * 106 * @param con the connection 107 * @throws OsmTransferException if something went wrong. Check for nested exceptions 108 */ 109 protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException { 110 CredentialsAgentResponse response; 111 try { 112 synchronized (CredentialsManager.getInstance()) { 113 response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER, 114 con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */); 115 } 116 } catch (CredentialsAgentException e) { 117 throw new OsmTransferException(e); 118 } 119 if (response != null) { 120 if (response.isCanceled()) { 121 cancel = true; 122 return; 123 } else { 124 String username = response.getUsername() == null ? "" : response.getUsername(); 125 String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword()); 126 String token = username + ':' + password; 127 con.setHeader("Authorization", BASIC_AUTH + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8))); 128 } 129 } 130 } 131 132 /** 133 * Signs the connection with an OAuth authentication header 134 * 135 * @param connection the connection 136 * 137 * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured 138 * @throws OsmTransferException if signing fails 139 */ 140 protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException { 141 if (oauthParameters == null) { 142 oauthParameters = OAuthParameters.createFromApiUrl(OsmApi.getOsmApi().getServerUrl()); 143 } 144 OAuthConsumer consumer = oauthParameters.buildConsumer(); 145 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance(); 146 if (!holder.containsAccessToken()) { 147 obtainAccessToken(connection); 148 } 149 if (!holder.containsAccessToken()) { // check if wizard completed 150 throw new MissingOAuthAccessTokenException(); 151 } 152 consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret()); 153 try { 154 consumer.sign(connection); 155 } catch (OAuthException e) { 156 throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e); 157 } 158 } 159 160 /** 161 * Obtains an OAuth access token for the connection. 162 * Afterwards, the token is accessible via {@link OAuthAccessTokenHolder} / {@link CredentialsManager}. 163 * @param connection connection for which the access token should be obtained 164 * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully 165 */ 166 protected void obtainAccessToken(final HttpClient connection) throws MissingOAuthAccessTokenException { 167 try { 168 final URL apiUrl = new URL(OsmApi.getOsmApi().getServerUrl()); 169 if (!Objects.equals(apiUrl.getHost(), connection.getURL().getHost())) { 170 throw new MissingOAuthAccessTokenException(); 171 } 172 fetcher.obtainAccessToken(apiUrl); 173 OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true); 174 OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance()); 175 } catch (MalformedURLException | InterruptedException | InvocationTargetException e) { 176 throw new MissingOAuthAccessTokenException(e); 177 } 178 } 179 180 protected void addAuth(HttpClient connection) throws OsmTransferException { 181 final String authMethod = OsmApi.getAuthMethod(); 182 if ("basic".equals(authMethod)) { 183 addBasicAuthorizationHeader(connection); 184 } else if ("oauth".equals(authMethod)) { 185 addOAuthAuthorizationHeader(connection); 186 } else { 187 String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod); 188 Logging.warn(msg); 189 throw new OsmTransferException(msg); 190 } 191 } 192 193 /** 194 * Replies true if this connection is canceled 195 * 196 * @return true if this connection is canceled 197 */ 198 public boolean isCanceled() { 199 return cancel; 200 } 201}