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