[6380] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[626] | 2 | package org.openstreetmap.josm.io;
|
---|
| 3 |
|
---|
[2748] | 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 5 |
|
---|
[9352] | 6 | import java.lang.reflect.InvocationTargetException;
|
---|
[6248] | 7 | import java.net.Authenticator.RequestorType;
|
---|
[9352] | 8 | import java.net.MalformedURLException;
|
---|
| 9 | import java.net.URL;
|
---|
[7082] | 10 | import java.nio.charset.StandardCharsets;
|
---|
[10618] | 11 | import java.util.Base64;
|
---|
[9352] | 12 | import java.util.Objects;
|
---|
[626] | 13 |
|
---|
[12686] | 14 | import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
|
---|
[2748] | 15 | import org.openstreetmap.josm.data.oauth.OAuthParameters;
|
---|
[4245] | 16 | import org.openstreetmap.josm.io.auth.CredentialsAgentException;
|
---|
[6248] | 17 | import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
|
---|
[4245] | 18 | import org.openstreetmap.josm.io.auth.CredentialsManager;
|
---|
[9172] | 19 | import org.openstreetmap.josm.tools.HttpClient;
|
---|
[12803] | 20 | import org.openstreetmap.josm.tools.JosmRuntimeException;
|
---|
[12620] | 21 | import org.openstreetmap.josm.tools.Logging;
|
---|
[626] | 22 |
|
---|
[8840] | 23 | import oauth.signpost.OAuthConsumer;
|
---|
| 24 | import oauth.signpost.exception.OAuthException;
|
---|
| 25 |
|
---|
[626] | 26 | /**
|
---|
| 27 | * Base class that handles common things like authentication for the reader and writer
|
---|
| 28 | * to the osm server.
|
---|
| 29 | *
|
---|
| 30 | * @author imi
|
---|
| 31 | */
|
---|
| 32 | public class OsmConnection {
|
---|
[12992] | 33 |
|
---|
| 34 | private static final String BASIC_AUTH = "Basic ";
|
---|
| 35 |
|
---|
[8840] | 36 | protected boolean cancel;
|
---|
[9309] | 37 | protected HttpClient activeConnection;
|
---|
[2748] | 38 | protected OAuthParameters oauthParameters;
|
---|
[626] | 39 |
|
---|
[1169] | 40 | /**
|
---|
[12803] | 41 | * Retrieves OAuth access token.
|
---|
| 42 | * @since 12803
|
---|
| 43 | */
|
---|
| 44 | public interface OAuthAccessTokenFetcher {
|
---|
| 45 | /**
|
---|
| 46 | * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
|
---|
| 47 | * @param serverUrl the URL to OSM server
|
---|
| 48 | * @throws InterruptedException if we're interrupted while waiting for the event dispatching thread to finish OAuth authorization task
|
---|
| 49 | * @throws InvocationTargetException if an exception is thrown while running OAuth authorization task
|
---|
| 50 | */
|
---|
| 51 | void obtainAccessToken(URL serverUrl) throws InvocationTargetException, InterruptedException;
|
---|
| 52 | }
|
---|
| 53 |
|
---|
[12869] | 54 | static volatile OAuthAccessTokenFetcher fetcher = u -> {
|
---|
[12803] | 55 | throw new JosmRuntimeException("OsmConnection.setOAuthAccessTokenFetcher() has not been called");
|
---|
| 56 | };
|
---|
| 57 |
|
---|
| 58 | /**
|
---|
| 59 | * Sets the OAuth access token fetcher.
|
---|
| 60 | * @param tokenFetcher new OAuth access token fetcher. Cannot be null
|
---|
| 61 | * @since 12803
|
---|
| 62 | */
|
---|
| 63 | public static void setOAuthAccessTokenFetcher(OAuthAccessTokenFetcher tokenFetcher) {
|
---|
| 64 | fetcher = Objects.requireNonNull(tokenFetcher, "tokenFetcher");
|
---|
| 65 | }
|
---|
| 66 |
|
---|
| 67 | /**
|
---|
[6643] | 68 | * Cancels the connection.
|
---|
| 69 | */
|
---|
[1169] | 70 | public void cancel() {
|
---|
| 71 | cancel = true;
|
---|
[2322] | 72 | synchronized (this) {
|
---|
| 73 | if (activeConnection != null) {
|
---|
| 74 | activeConnection.disconnect();
|
---|
| 75 | }
|
---|
| 76 | }
|
---|
[1169] | 77 | }
|
---|
[626] | 78 |
|
---|
[2748] | 79 | /**
|
---|
[12992] | 80 | * Retrieves login from basic authentication header, if set.
|
---|
| 81 | *
|
---|
| 82 | * @param con the connection
|
---|
| 83 | * @return login from basic authentication header, or {@code null}
|
---|
| 84 | * @throws OsmTransferException if something went wrong. Check for nested exceptions
|
---|
| 85 | * @since 12992
|
---|
| 86 | */
|
---|
| 87 | protected String retrieveBasicAuthorizationLogin(HttpClient con) throws OsmTransferException {
|
---|
| 88 | String auth = con.getRequestHeader("Authorization");
|
---|
| 89 | if (auth != null && auth.startsWith(BASIC_AUTH)) {
|
---|
| 90 | try {
|
---|
| 91 | String[] token = new String(Base64.getDecoder().decode(auth.substring(BASIC_AUTH.length())),
|
---|
| 92 | StandardCharsets.UTF_8).split(":");
|
---|
| 93 | if (token.length == 2) {
|
---|
| 94 | return token[0];
|
---|
| 95 | }
|
---|
| 96 | } catch (IllegalArgumentException e) {
|
---|
| 97 | Logging.error(e);
|
---|
| 98 | }
|
---|
| 99 | }
|
---|
| 100 | return null;
|
---|
| 101 | }
|
---|
| 102 |
|
---|
| 103 | /**
|
---|
[2748] | 104 | * Adds an authentication header for basic authentication
|
---|
[2801] | 105 | *
|
---|
[2748] | 106 | * @param con the connection
|
---|
[8291] | 107 | * @throws OsmTransferException if something went wrong. Check for nested exceptions
|
---|
[2748] | 108 | */
|
---|
[9172] | 109 | protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException {
|
---|
[4245] | 110 | CredentialsAgentResponse response;
|
---|
[1955] | 111 | try {
|
---|
[4245] | 112 | synchronized (CredentialsManager.getInstance()) {
|
---|
[4690] | 113 | response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
|
---|
| 114 | con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
|
---|
[1955] | 115 | }
|
---|
[4245] | 116 | } catch (CredentialsAgentException e) {
|
---|
[2641] | 117 | throw new OsmTransferException(e);
|
---|
[1955] | 118 | }
|
---|
[11544] | 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;
|
---|
[12992] | 127 | con.setHeader("Authorization", BASIC_AUTH + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8)));
|
---|
[11544] | 128 | }
|
---|
[2641] | 129 | }
|
---|
[1169] | 130 | }
|
---|
[1881] | 131 |
|
---|
| 132 | /**
|
---|
[2748] | 133 | * Signs the connection with an OAuth authentication header
|
---|
[2801] | 134 | *
|
---|
[2748] | 135 | * @param connection the connection
|
---|
[2801] | 136 | *
|
---|
[12470] | 137 | * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured
|
---|
[8291] | 138 | * @throws OsmTransferException if signing fails
|
---|
[2748] | 139 | */
|
---|
[9172] | 140 | protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException {
|
---|
[2748] | 141 | if (oauthParameters == null) {
|
---|
[12928] | 142 | oauthParameters = OAuthParameters.createFromApiUrl(OsmApi.getOsmApi().getServerUrl());
|
---|
[2748] | 143 | }
|
---|
| 144 | OAuthConsumer consumer = oauthParameters.buildConsumer();
|
---|
| 145 | OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
|
---|
[9352] | 146 | if (!holder.containsAccessToken()) {
|
---|
| 147 | obtainAccessToken(connection);
|
---|
| 148 | }
|
---|
| 149 | if (!holder.containsAccessToken()) { // check if wizard completed
|
---|
[2862] | 150 | throw new MissingOAuthAccessTokenException();
|
---|
[9352] | 151 | }
|
---|
[2748] | 152 | consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret());
|
---|
| 153 | try {
|
---|
| 154 | consumer.sign(connection);
|
---|
[8510] | 155 | } catch (OAuthException e) {
|
---|
[2748] | 156 | throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
|
---|
| 157 | }
|
---|
| 158 | }
|
---|
| 159 |
|
---|
[9352] | 160 | /**
|
---|
[12803] | 161 | * Obtains an OAuth access token for the connection.
|
---|
| 162 | * Afterwards, the token is accessible via {@link OAuthAccessTokenHolder} / {@link CredentialsManager}.
|
---|
[9352] | 163 | * @param connection connection for which the access token should be obtained
|
---|
[12803] | 164 | * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully
|
---|
[9352] | 165 | */
|
---|
| 166 | protected void obtainAccessToken(final HttpClient connection) throws MissingOAuthAccessTokenException {
|
---|
| 167 | try {
|
---|
[9353] | 168 | final URL apiUrl = new URL(OsmApi.getOsmApi().getServerUrl());
|
---|
[9352] | 169 | if (!Objects.equals(apiUrl.getHost(), connection.getURL().getHost())) {
|
---|
| 170 | throw new MissingOAuthAccessTokenException();
|
---|
| 171 | }
|
---|
[12803] | 172 | fetcher.obtainAccessToken(apiUrl);
|
---|
| 173 | OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true);
|
---|
[12928] | 174 | OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
|
---|
[9352] | 175 | } catch (MalformedURLException | InterruptedException | InvocationTargetException e) {
|
---|
[10237] | 176 | throw new MissingOAuthAccessTokenException(e);
|
---|
[9352] | 177 | }
|
---|
| 178 | }
|
---|
| 179 |
|
---|
[9172] | 180 | protected void addAuth(HttpClient connection) throws OsmTransferException {
|
---|
[9352] | 181 | final String authMethod = OsmApi.getAuthMethod();
|
---|
[7012] | 182 | if ("basic".equals(authMethod)) {
|
---|
[2748] | 183 | addBasicAuthorizationHeader(connection);
|
---|
[7012] | 184 | } else if ("oauth".equals(authMethod)) {
|
---|
[2748] | 185 | addOAuthAuthorizationHeader(connection);
|
---|
| 186 | } else {
|
---|
[6248] | 187 | String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
|
---|
[12620] | 188 | Logging.warn(msg);
|
---|
[2748] | 189 | throw new OsmTransferException(msg);
|
---|
| 190 | }
|
---|
| 191 | }
|
---|
| 192 |
|
---|
| 193 | /**
|
---|
[1881] | 194 | * Replies true if this connection is canceled
|
---|
[2512] | 195 | *
|
---|
[1881] | 196 | * @return true if this connection is canceled
|
---|
| 197 | */
|
---|
| 198 | public boolean isCanceled() {
|
---|
| 199 | return cancel;
|
---|
| 200 | }
|
---|
[626] | 201 | }
|
---|