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.net.Authenticator.RequestorType;
007import java.net.HttpURLConnection;
008import java.nio.ByteBuffer;
009import java.nio.CharBuffer;
010import java.nio.charset.CharacterCodingException;
011import java.nio.charset.CharsetEncoder;
012import java.nio.charset.StandardCharsets;
013
014import oauth.signpost.OAuthConsumer;
015import oauth.signpost.exception.OAuthException;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.oauth.OAuthParameters;
019import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
020import org.openstreetmap.josm.io.auth.CredentialsAgentException;
021import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
022import org.openstreetmap.josm.io.auth.CredentialsManager;
023import org.openstreetmap.josm.tools.Base64;
024
025/**
026 * Base class that handles common things like authentication for the reader and writer
027 * to the osm server.
028 *
029 * @author imi
030 */
031public class OsmConnection {
032    protected boolean cancel = false;
033    protected HttpURLConnection activeConnection;
034    protected OAuthParameters oauthParameters;
035
036    /**
037     * Initialize the http defaults and the authenticator.
038     */
039    static {
040        try {
041            HttpURLConnection.setFollowRedirects(true);
042        } catch (SecurityException e) {
043            Main.error(e);
044        }
045    }
046
047    /**
048     * Cancels the connection.
049     */
050    public void cancel() {
051        cancel = true;
052        synchronized (this) {
053            if (activeConnection != null) {
054                activeConnection.setConnectTimeout(100);
055                activeConnection.setReadTimeout(100);
056            }
057        }
058        try {
059            Thread.sleep(100);
060        } catch (InterruptedException ex) {
061            Main.warn("InterruptedException in "+getClass().getSimpleName()+" during cancel");
062        }
063
064        synchronized (this) {
065            if (activeConnection != null) {
066                activeConnection.disconnect();
067            }
068        }
069    }
070
071    /**
072     * Adds an authentication header for basic authentication
073     *
074     * @param con the connection
075     * @throws OsmTransferException thrown if something went wrong. Check for nested exceptions
076     */
077    protected void addBasicAuthorizationHeader(HttpURLConnection con) throws OsmTransferException {
078        CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
079        CredentialsAgentResponse response;
080        String token;
081        try {
082            synchronized (CredentialsManager.getInstance()) {
083                response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
084                con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
085            }
086        } catch (CredentialsAgentException e) {
087            throw new OsmTransferException(e);
088        }
089        if (response == null) {
090            token = ":";
091        } else if (response.isCanceled()) {
092            cancel = true;
093            return;
094        } else {
095            String username= response.getUsername() == null ? "" : response.getUsername();
096            String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword());
097            token = username + ":" + password;
098            try {
099                ByteBuffer bytes = encoder.encode(CharBuffer.wrap(token));
100                con.addRequestProperty("Authorization", "Basic "+Base64.encode(bytes));
101            } catch(CharacterCodingException e) {
102                throw new OsmTransferException(e);
103            }
104        }
105    }
106
107    /**
108     * Signs the connection with an OAuth authentication header
109     *
110     * @param connection the connection
111     *
112     * @throws OsmTransferException thrown if there is currently no OAuth Access Token configured
113     * @throws OsmTransferException thrown if signing fails
114     */
115    protected void addOAuthAuthorizationHeader(HttpURLConnection connection) throws OsmTransferException {
116        if (oauthParameters == null) {
117            oauthParameters = OAuthParameters.createFromPreferences(Main.pref);
118        }
119        OAuthConsumer consumer = oauthParameters.buildConsumer();
120        OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
121        if (! holder.containsAccessToken())
122            throw new MissingOAuthAccessTokenException();
123        consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret());
124        try {
125            consumer.sign(connection);
126        } catch(OAuthException e) {
127            throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
128        }
129    }
130
131    protected void addAuth(HttpURLConnection connection) throws OsmTransferException {
132        String authMethod = Main.pref.get("osm-server.auth-method", "basic");
133        if ("basic".equals(authMethod)) {
134            addBasicAuthorizationHeader(connection);
135        } else if ("oauth".equals(authMethod)) {
136            addOAuthAuthorizationHeader(connection);
137        } else {
138            String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
139            Main.warn(msg);
140            throw new OsmTransferException(msg);
141        }
142    }
143
144    /**
145     * Replies true if this connection is canceled
146     *
147     * @return true if this connection is canceled
148     */
149    public boolean isCanceled() {
150        return cancel;
151    }
152}