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(":", -1); 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 } else { 123 String username = response.getUsername() == null ? "" : response.getUsername(); 124 String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword()); 125 String token = username + ':' + password; 126 con.setHeader("Authorization", BASIC_AUTH + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8))); 127 } 128 } 129 } 130 131 /** 132 * Signs the connection with an OAuth authentication header 133 * 134 * @param connection the connection 135 * 136 * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured 137 * @throws OsmTransferException if signing fails 138 */ 139 protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException { 140 if (oauthParameters == null) { 141 oauthParameters = OAuthParameters.createFromApiUrl(OsmApi.getOsmApi().getServerUrl()); 142 } 143 OAuthConsumer consumer = oauthParameters.buildConsumer(); 144 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance(); 145 if (!holder.containsAccessToken()) { 146 obtainAccessToken(connection); 147 } 148 if (!holder.containsAccessToken()) { // check if wizard completed 149 throw new MissingOAuthAccessTokenException(); 150 } 151 consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret()); 152 try { 153 consumer.sign(connection); 154 } catch (OAuthException e) { 155 throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e); 156 } 157 } 158 159 /** 160 * Obtains an OAuth access token for the connection. 161 * Afterwards, the token is accessible via {@link OAuthAccessTokenHolder} / {@link CredentialsManager}. 162 * @param connection connection for which the access token should be obtained 163 * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully 164 */ 165 protected void obtainAccessToken(final HttpClient connection) throws MissingOAuthAccessTokenException { 166 try { 167 final URL apiUrl = new URL(OsmApi.getOsmApi().getServerUrl()); 168 if (!Objects.equals(apiUrl.getHost(), connection.getURL().getHost())) { 169 throw new MissingOAuthAccessTokenException(); 170 } 171 fetcher.obtainAccessToken(apiUrl); 172 OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true); 173 OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance()); 174 } catch (MalformedURLException | InterruptedException | InvocationTargetException e) { 175 throw new MissingOAuthAccessTokenException(e); 176 } 177 } 178 179 protected void addAuth(HttpClient connection) throws OsmTransferException { 180 final String authMethod = OsmApi.getAuthMethod(); 181 if ("basic".equals(authMethod)) { 182 addBasicAuthorizationHeader(connection); 183 } else if ("oauth".equals(authMethod)) { 184 addOAuthAuthorizationHeader(connection); 185 } else { 186 String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod); 187 Logging.warn(msg); 188 throw new OsmTransferException(msg); 189 } 190 } 191 192 /** 193 * Replies true if this connection is canceled 194 * 195 * @return true if this connection is canceled 196 */ 197 public boolean isCanceled() { 198 return cancel; 199 } 200}